This commit is contained in:
Mario Peters
2025-06-09 18:11:06 +02:00
parent 57db35c597
commit 31750df773
11 changed files with 272 additions and 198 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "kt-vite",
"version": "3.3.0",
"version": "3.4.1",
"private": true,
"homepage": "https://karateturniere.de",
"type": "module",
@@ -57,4 +57,4 @@
"vite-plugin-eslint": "^1.8.1",
"vitest": "^3.2.2"
}
}
}

View File

@@ -1,5 +1,5 @@
// @ts-check
import { defineConfig, devices } from '@playwright/test';
import { defineConfig, devices } from '@playwright/test'
/**
* Read environment variables from file.
@@ -13,69 +13,68 @@ import { defineConfig, devices } from '@playwright/test';
* @see https://playwright.dev/docs/test-configuration
*/
export default defineConfig({
testDir: './test/e2e/',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
testDir: './test/e2e/',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run backend',
// url: 'http://localhost:8000',
// reuseExistingServer: !process.env.CI,
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://localhost:3000',
// reuseExistingServer: !process.env.CI,
// },
});
})

View File

@@ -85,7 +85,7 @@ export default function App() {
}, [dialogOpen, dialogContent, dialogMaxWidth])
const openLoginDialog = (view) => {
dialog.setContent(<LoginDialog handleToken={handleToken} apiServer={apiServer} user={user} view={view} token={token} />)
dialog.setContent(<LoginDialog handleToken={handleToken} view={view} />)
dialog.setOpen()
}

View File

@@ -63,14 +63,12 @@ export async function signUpEdit(apiServer, token, newData) {
export async function login(apiServer, email, password, fakeID) {
const url = apiServer + '/authentications'
const data = { login: { email, password, fakeID } }
console.log('Request URL:', url)
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(data),
mode: 'cors',
headers: { 'Content-Type': 'application/json' },
})
console.log('Response status:', response.status)
if (response.status === 200) {
return {
account: response.headers.get('x-kt-account'),

View File

@@ -11,7 +11,6 @@ export default function LoginDialog({ handleToken, view }) {
const dialog = useDialog()
const { t } = useTranslation('common')
const [state, setState] = useState(view || 'Login')
console.log('LoginDialog state', state)
return (
<>

View File

@@ -1,7 +1,23 @@
import { useEffect, useState } from 'react'
import useFetch from '../UseFetch/UseFetch'
import { useDialog } from '../../context/DialogContext'
import { Button, TextField, DialogActions, DialogContent, DialogTitle, Radio, RadioGroup, FormControlLabel, FormControl, FormLabel, InputLabel, MenuItem, Select, Autocomplete } from '@mui/material'
import {
Button,
TextField,
DialogActions,
DialogContent,
DialogTitle,
Radio,
RadioGroup,
FormControlLabel,
FormControl,
FormLabel,
InputLabel,
MenuItem,
Select,
Autocomplete,
CircularProgress,
} from '@mui/material'
import { useTranslation } from 'react-i18next'
import { addParticipant, editParticipant } from '../../api/participants.js'
import { BeltClass } from '../../utilities/Belt'
@@ -59,14 +75,11 @@ export default function AddEditParticipant({ edit, setEdit }) {
let updated
if (edit) {
await editParticipant(apiServer, token, formData)
// Teilnehmer in der Liste ersetzen
console.log('participants', participants)
updated = participants.map((p) => (p.id === formData.id ? { ...formData } : p))
} else {
const newParticipant = await addParticipant(apiServer, token, formData)
// Teilnehmer zur Liste hinzufügen
formData.id = newParticipant.insertId
updated = [...(participants || []), formData]
const participantWithId = { ...formData, id: newParticipant.insertId }
updated = [...(participants || []), participantWithId]
}
setParticipants(updated)
dialog.onClose()
@@ -75,64 +88,70 @@ export default function AddEditParticipant({ edit, setEdit }) {
}
}
if (loading) {
return (
<DialogContent>
<CircularProgress />
</DialogContent>
)
}
return (
!loading && (
<>
<DialogTitle id="dialog-title">{edit ? t('participant.edit-participant') : t('participant.add-participant')}</DialogTitle>
<DialogContent>
<TextField autoFocus margin="dense" name="name" variant="standard" label={t('participant.name')} type="text" fullWidth value={formData.name} onChange={handleChange} />
<TextField margin="dense" name="vorname" variant="standard" label={t('participant.forename')} type="text" fullWidth value={formData.vorname} onChange={handleChange} />
<Autocomplete
id="club"
freeSolo
options={clubs}
value={formData.verein}
onChange={handleClubChange}
renderInput={(params) => <TextField {...params} variant="standard" label={t('participant.club')} margin="dense" />}
/>
<FormControl variant="standard" fullWidth margin="dense">
<InputLabel id="gurt-label">{t('participant.belt')}</InputLabel>
<Select labelId="gurt-label" id="gurt" name="gurt" value={formData.gurt} onChange={handleChange}>
{BELTS.map((belt) => (
<MenuItem key={belt} value={belt} className={BeltClass(belt)}>
{belt}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
margin="dense"
name="gebDatum"
variant="standard"
label={t('participant.birthDate')}
type="date"
fullWidth
value={formData.gebDatum}
onChange={handleChange}
<>
<DialogTitle id="dialog-title">{edit ? t('participant.edit-participant') : t('participant.add-participant')}</DialogTitle>
<DialogContent>
<TextField autoFocus margin="dense" name="name" variant="standard" label={t('participant.name')} type="text" fullWidth value={formData.name} onChange={handleChange} />
<TextField margin="dense" name="vorname" variant="standard" label={t('participant.forename')} type="text" fullWidth value={formData.vorname} onChange={handleChange} />
<Autocomplete
id="club"
freeSolo
options={clubs}
value={formData.verein}
onChange={handleClubChange}
renderInput={(params) => <TextField {...params} variant="standard" label={t('participant.club')} margin="dense" />}
/>
<FormControl variant="standard" fullWidth margin="dense">
<InputLabel id="gurt-label">{t('participant.belt')}</InputLabel>
<Select labelId="gurt-label" id="gurt" name="gurt" value={formData.gurt} onChange={handleChange}>
{BELTS.map((belt) => (
<MenuItem key={belt} value={belt} className={BeltClass(belt)}>
{belt}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
margin="dense"
name="gebDatum"
variant="standard"
label={t('participant.birthDate')}
type="date"
fullWidth
value={formData.gebDatum}
onChange={handleChange}
slotProps={{
inputLabel: { shrink: true },
}}
/>
<FormControl component="fieldset" margin="dense">
<FormLabel component="legend">{t('participant.gender')}</FormLabel>
<RadioGroup aria-label="gender" name="geschlecht" value={formData.geschlecht} onChange={handleChange} row>
<FormControlLabel value="W" control={<Radio />} label={t('participant.female')} />
<FormControlLabel value="M" control={<Radio color="primary" />} label={t('participant.male')} />
</RadioGroup>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={handleReset} color="secondary">
{t('reset')}
</Button>
<Button onClick={handleClose} color="secondary">
{t('cancel')}
</Button>
<Button onClick={handleSave} variant="outlined" color="primary">
OK
</Button>
</DialogActions>
</>
)
}}
/>
<FormControl component="fieldset" margin="dense">
<FormLabel component="legend">{t('participant.gender')}</FormLabel>
<RadioGroup aria-label="gender" name="geschlecht" value={formData.geschlecht} onChange={handleChange} row>
<FormControlLabel value="W" control={<Radio />} label={t('participant.female')} />
<FormControlLabel value="M" control={<Radio color="primary" />} label={t('participant.male')} />
</RadioGroup>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={handleReset} color="secondary">
{t('reset')}
</Button>
<Button onClick={handleClose} color="secondary">
{t('cancel')}
</Button>
<Button onClick={handleSave} variant="outlined" color="primary">
OK
</Button>
</DialogActions>
</>
)
}

View File

@@ -1,6 +1,7 @@
import ResultsEntry from './ResultsEntry'
import { Chip, Avatar } from '@mui/material'
import { styled } from '@mui/material-pigment-css'
import { useUser } from '../../context/UserContext'
const PREFIX = 'ResultsEntryContainer'
const classes = {
@@ -34,7 +35,8 @@ const Root = styled('div')(() => ({
},
}))
export default function ResultsEntryContainer({ el, team, place, user }) {
export default function ResultsEntryContainer({ el, team, place }) {
const { user } = useUser()
const gender = el.geschlecht === 'M' ? 'primary' : 'secondary'
const places = [
{ p: place.p1, place: 1 },

View File

@@ -1,7 +1,7 @@
import { useUser } from '../../context/UserContext'
import ResultsEntryContainer from '../Results/ResultsEntryContainer'
export default function TournamentResults({ groups, user, teams }) {
export default function TournamentResults({ groups, teams }) {
const { participants } = useUser()
const getParticipant = (id) => participants.find((x) => x.id == id)
const getTeam = (id) => teams.find((x) => x.id == id)
@@ -19,6 +19,6 @@ export default function TournamentResults({ groups, user, teams }) {
p4: results ? (isTeam ? getTeam(results.p4) : getParticipant(results.p4)) : isTeam ? getTeam(el.platz4team) : getParticipant(el.platz4),
}
return <ResultsEntryContainer key={el.id} el={el} team={isTeam} place={place} user={user} />
return <ResultsEntryContainer key={el.id} el={el} team={isTeam} place={place} />
})
}

View File

@@ -4,6 +4,7 @@ import { useDialog } from '../../context/DialogContext'
import { useTranslation } from 'react-i18next'
import { addTournament, editTournament } from '../../api/tournaments.js'
import { styled } from '@mui/material-pigment-css'
import { useUser } from '../../context/UserContext.jsx'
const PREFIX = 'AddEditTournament'
const classes = {
@@ -53,7 +54,8 @@ function toDateInput(val) {
}
}
export default function AddEditTournament({ token, apiServer, tournament, tournaments, setTournaments }) {
export default function AddEditTournament({ tournament, tournaments, setTournaments }) {
const { apiServer, token } = useUser()
const { t } = useTranslation('common')
const [formData, setFormData] = useState(initialFormData)
const dialog = useDialog()

View File

@@ -67,12 +67,12 @@ export default function Tournaments({ groups, tournaments, setTournaments }) {
<Root>
{currentTournaments.map((t) => (
<div key={t.id}>
<TournamentsCard tournaments={tournaments} setTournaments={setTournaments} user={user} tournament={t} tournamentGroups={filterObject('turnier_id', t.id, groups)} />
<TournamentsCard tournaments={tournaments} setTournaments={setTournaments} tournament={t} tournamentGroups={filterObject('turnier_id', t.id, groups)} />
</div>
))}
{olderTournaments.map((t) => (
<div className={classes.olderEntry} key={t.id}>
<TournamentsCard tournaments={tournaments} setTournaments={setTournaments} user={user} tournament={t} tournamentGroups={filterObject('turnier_id', t.id, groups)} />
<TournamentsCard tournaments={tournaments} setTournaments={setTournaments} tournament={t} tournamentGroups={filterObject('turnier_id', t.id, groups)} />
</div>
))}
{user?.admin > 4 && (

View File

@@ -1,83 +1,138 @@
// e2e/basic.spec.js
import { test, expect } from '@playwright/test'
const login = async (page, email = 'mario@wattsche.de', password = 'mmario84') => {
await page.goto('http://localhost:5173/')
await page.getByRole('banner').getByRole('button').nth(3).click()
await page.getByRole('textbox', { name: 'Email' }).fill(email)
await page.getByRole('textbox', { name: 'Passwort' }).fill(password)
await page.getByRole('main').getByRole('button').click()
// Optional: Warte auf ein Element, das nur nach Login sichtbar ist
await expect(page.getByText('Alexander Zott')).toBeTruthy()
}
const participants = [
{ name: 'test-user', vorname: '123', verein: 'KD tv remagen', guertel: '8. Kyu', geburtstag: '1111-11-11' },
{ name: 'test-user', vorname: '321', verein: 'DJKB', guertel: '6. Kyu', geburtstag: '1999-10-10' },
{ name: 'test-user', vorname: '456', verein: 'KD tv remagen', guertel: 'DAN', geburtstag: '1980-01-01' },
{ name: 'test-user', vorname: '789', verein: 'KD tv remagen', guertel: '1. Kyu', geburtstag: '2000-01-01' },
{ name: 'test-user', vorname: '000', verein: 'Ippon Frankfurt', guertel: '2. Kyu', geburtstag: '1999-10-10' },
{ name: 'test-user', vorname: '111', verein: 'KD FUNAKOSHI TROISDORF e.V.', guertel: '3. Kyu', geburtstag: '1999-10-10' },
{ name: 'test-user', vorname: '222', verein: 'KD tv remagen', guertel: '4. Kyu', geburtstag: '1999-10-10' },
{ name: 'test-user', vorname: '333', verein: 'KD Groß-Umstadt', guertel: '5. Kyu', geburtstag: '1999-10-10' },
]
test('Startseite lädt', async ({ page }) => {
const testStartseiteLaedt = async ({ page }) => {
await page.goto('http://localhost:5173')
console.log(await page.title())
await page.pause()
await expect(page).toHaveTitle('Karateturniere.de')
})
}
const login = async (page, email = 'mario@wattsche.de', password = 'mmario84') => {
await page.goto('http://localhost:5173/')
await page.evaluate(() => window.localStorage.clear())
await page.evaluate(() => window.sessionStorage.clear())
await page.getByRole('banner').getByRole('button').nth(3).click()
await page.getByRole('textbox', { name: 'E-Mail' }).fill(email)
await page.getByRole('textbox', { name: 'Passwort' }).fill(password)
await page.getByRole('main').getByRole('button').click()
await expect(page.getByText('Alexander Zott')).toBeTruthy()
}
const fillAddParticipantForm = async ({ page, participant }) => {
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(participant.name)
await page.getByRole('textbox', { name: 'Vorname' }).fill(participant.vorname)
await page.getByRole('combobox', { name: 'Verein' }).click()
await page.getByRole('option', { name: participant.verein }).click()
await page.getByRole('combobox', { name: 'Gürtel' }).click()
await page.getByRole('option', { name: participant.guertel }).click()
await page.getByRole('textbox', { name: 'Geburtstag' }).fill(participant.geburtstag)
await page.getByRole('button', { name: 'OK' }).click()
}
const testTeilnehmerHinzufuegen = async ({ page, i }) => {
const name = participants[i].vorname + ' ' + participants[i].name
await login(page)
await page.getByTestId('add-participant-button').click()
// // Alle Teilnehmer anlegen
// participants.forEach(async (participant) => {
// await fillAddParticipantForm({ page, participant })
// })
// Ein Teilnehmer hinzufügen
await fillAddParticipantForm({ page, participant: participants[i] })
await page.getByRole('textbox', { name: 'search' }).click()
await page.getByRole('textbox', { name: 'search' }).fill(name)
console.log(await page.getByTestId('PersonIcon'))
await expect(page.getByText(name)).toBeTruthy()
}
const testBearbeiten = async ({ page, i }) => {
const name = participants[i].vorname + ' ' + participants[i].name
const nameNew = participants[i + 1].vorname + ' ' + participants[i + 1].name
await login(page)
await page.getByRole('textbox', { name: 'search' }).fill(name)
await page.locator('svg[data-testid="EditIcon"]').click()
await fillAddParticipantForm({ page, participant: participants[i + 1] })
await page.getByRole('textbox', { name: 'search' }).fill(nameNew)
await expect(page.getByText(name)).toBeTruthy()
}
const testTeilnehmerLoeschen = async ({ page, i }) => {
const name = participants[i].vorname + ' ' + participants[i].name
await login(page)
await page.getByRole('textbox', { name: 'search' }).click()
await page.getByRole('textbox', { name: 'search' }).fill(name)
await page.locator('svg[data-testid="DeleteOutlineIcon"]').click()
await page.getByRole('button', { name: 'Delete' }).click()
await page.waitForTimeout(1000)
await expect(page.getByText(name)).toHaveCount(0)
}
const testTeilnehmerAnmelden = async ({ page }) => {
await login(page)
await page.getByRole('button', { name: 'Anmeldung' }).first().click()
// Einzel
await page.locator('input[data-id="325"][value="Kata"]').click()
await page.locator('input[data-id="325"][value="Kumite"]').click()
await page.locator('input[data-id="327"][value="Kumite"]').click()
await page.locator('input[data-id="327"][value="Kumite"]').click()
// Team
const m1 = await page.locator('div .TeamRegistrationMobile-counter').nth(0).textContent()
const m2 = await page.locator('div .TeamRegistrationMobile-counter').nth(1).textContent()
await page.getByTestId('AddIcon').nth(0).click()
await page.getByTestId('AddIcon').nth(1).click()
await page.waitForTimeout(1000)
await expect(page.locator('div .TeamRegistrationMobile-counter').nth(0)).toHaveText(String(+m1 + 1))
await expect(page.locator('div .TeamRegistrationMobile-counter').nth(1)).toHaveText(String(+m2 + 1))
// reset
await page.locator('input[data-id="325"][value="Kata"]').click()
await page.locator('input[data-id="325"][value="Kumite"]').click()
await page.locator('input[data-id="327"][value="Kumite"]').click()
await page.locator('input[data-id="327"][value="Kumite"]').click()
await page.getByTestId('RemoveIcon').nth(0).click()
await page.getByTestId('RemoveIcon').nth(1).click()
await page.waitForTimeout(1000)
await expect(page.locator('div .TeamRegistrationMobile-counter').nth(0)).toHaveText(String(+m1))
await expect(page.locator('div .TeamRegistrationMobile-counter').nth(1)).toHaveText(String(+m2))
}
// Tests registrieren
test('Startseite lädt', testStartseiteLaedt)
test('login', async ({ page }) => {
await login(page)
})
test('Teilnehmer hinzufügen', async ({ page }) => {
await login(page)
await page.getByTestId('add-participant-button').click()
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('qqq')
await page.getByRole('textbox', { name: 'Vorname' }).fill('qqq')
await page.getByRole('textbox', { name: 'Vorname' }).press('Tab')
await page.getByRole('combobox', { name: 'Verein' }).click()
await page.getByRole('option', { name: 'KD tv remagen' }).click()
await page.getByRole('combobox', { name: 'Gürtel' }).click()
await page.getByRole('option', { name: '8. Kyu' }).click()
await page.getByRole('textbox', { name: 'Geburtstag' }).fill('1111-11-11')
await page.getByRole('button', { name: 'OK' }).click()
await page.getByRole('textbox', { name: 'search' }).click()
await page.getByRole('textbox', { name: 'search' }).fill('qqq')
await page.getByText('qqq qqq')
expect(await page.getByText('qqq qqq')).toBeTruthy()
await testTeilnehmerHinzufuegen({ page, i: 0 })
})
test('teilnehmer löschen', async ({ page }) => {
await login(page)
await page.getByRole('textbox', { name: 'search' }).click()
await page.getByRole('textbox', { name: 'search' }).fill('qqq')
await page
.locator('div')
.filter({ hasText: /^qqq qqq$/ })
.getByRole('button')
.nth(1)
.click()
await page.getByRole('button', { name: 'Delete' }).click()
expect(await page.getByText('qqq qqq')).toBeFalsy()
test('Teilnehmer bearbeiten', async ({ page }) => {
await testBearbeiten({ page, i: 0 })
})
test('Teilnehmer löschen', async ({ page }) => {
await testTeilnehmerLoeschen({ page, i: 1 })
})
test('Teilnehmer anmelden', async ({ page }) => {
await login(page)
await page.getByRole('button', { name: 'Anmeldung' }).first().click()
await page.locator('input[data-id="325"][value="Kata"]').click()
await page.locator('input[data-id="325"][value="Kumite"]').click()
await page.locator('input[data-id="327"][value="Kumite"]').click()
await page.locator('input[data-id="327"][value="Kumite"]').click()
await page
.locator('div')
.filter({ hasText: /^45448\. Kyu91311\.11\.1111qqq qqq1 KD tv remagen$/ })
.getByRole('button')
.nth(1)
await page
.locator('button')
.filter({ hasText: /^2492Kata-Team-Mixed7\. Kyu - DANage12 - 13$/ })
.getByLabel('register Team', { exact: true })
.click()
await page.locator('div').filter({ hasText: /^3$/ }).click()
await expect(page.getByRole('main')).toContainText('3')
await expect(page.getByRole('main')).toContainText('2')
await testTeilnehmerAnmelden({ page })
})