Minor fixes
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package com.peakup.activity.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
@@ -110,24 +111,29 @@ public class IntervalsActivity extends AbstractActivity {
|
||||
@JdbcTypeCode(SqlTypes.ARRAY)
|
||||
public @Nullable int[] recordingStops;
|
||||
|
||||
@JsonIgnore
|
||||
@ElementCollection(fetch = FetchType.LAZY)
|
||||
@CollectionTable(name = "activity_achievement", joinColumns = @JoinColumn(name = "activity_id"))
|
||||
public List<ActivityAchievement> achievements = new ArrayList<>();
|
||||
|
||||
@JsonIgnore
|
||||
@ElementCollection(fetch = FetchType.LAZY)
|
||||
@CollectionTable(name = "activity_zone", joinColumns = @JoinColumn(name = "activity_id"))
|
||||
public List<ActivityZone> zones = new ArrayList<>();
|
||||
|
||||
@JsonIgnore
|
||||
@ElementCollection(fetch = FetchType.LAZY)
|
||||
@CollectionTable(name = "activity_ignore_part", joinColumns = @JoinColumn(name = "activity_id"))
|
||||
@OrderColumn(name = "sort_index")
|
||||
public List<IgnorePart> ignoreParts = new ArrayList<>();
|
||||
|
||||
@JsonIgnore
|
||||
@ElementCollection(fetch = FetchType.LAZY)
|
||||
@CollectionTable(name = "activity_tag", joinColumns = @JoinColumn(name = "activity_id"))
|
||||
@Column(name = "tag")
|
||||
public List<String> tags = new ArrayList<>();
|
||||
|
||||
@JsonIgnore
|
||||
@ElementCollection(fetch = FetchType.LAZY)
|
||||
@CollectionTable(name = "activity_attachment", joinColumns = @JoinColumn(name = "activity_id"))
|
||||
@OrderColumn(name = "sort_index")
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.peakup.common;
|
||||
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.container.ContainerResponseContext;
|
||||
import jakarta.ws.rs.container.ContainerResponseFilter;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
|
||||
@Provider
|
||||
public class CorsFilter implements ContainerResponseFilter {
|
||||
|
||||
@Override
|
||||
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
|
||||
responseContext.getHeaders().add("Access-Control-Allow-Origin", "*");
|
||||
responseContext.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH");
|
||||
responseContext.getHeaders().add("Access-Control-Allow-Headers", "accept, authorization, content-type, x-requested-with");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
quarkus.http.port=8111
|
||||
quarkus.http.cors=true
|
||||
quarkus.http.cors.origins=http://localhost:5173
|
||||
quarkus.scheduler.enabled=true
|
||||
|
||||
quarkus.datasource.db-kind=postgresql
|
||||
|
||||
180
peakup-frontend/e2e/activities.spec.ts
Normal file
180
peakup-frontend/e2e/activities.spec.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { setupApiMocks, login, MOCK_ACTIVITIES } from './fixtures'
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupApiMocks(page)
|
||||
await login(page)
|
||||
await page.getByRole('button', { name: 'ACTIVITIES' }).click()
|
||||
await expect(page.getByRole('heading', { name: 'Activities' })).toBeVisible()
|
||||
})
|
||||
|
||||
// ── Header ────────────────────────────────────────────────────────────────────
|
||||
|
||||
test('renders page header and activity count', async ({ page }) => {
|
||||
await expect(page.getByText('Training Log')).toBeVisible()
|
||||
await expect(page.getByText('Activities', { exact: true }).first()).toBeVisible()
|
||||
await expect(page.getByText(`${MOCK_ACTIVITIES.length} ACTIVITIES`)).toBeVisible()
|
||||
})
|
||||
|
||||
// ── Table columns ─────────────────────────────────────────────────────────────
|
||||
|
||||
test('renders all 9 table column headers', async ({ page }) => {
|
||||
const headers = ['DATE', 'ACTIVITY', 'TYPE', 'DISTANCE', 'TIME', 'AVG HR', 'AVG POWER', 'ELEV.', 'TSS']
|
||||
for (const header of headers) {
|
||||
await expect(page.getByRole('columnheader', { name: header })).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('table shows 20 rows on first page (page size = 20)', async ({ page }) => {
|
||||
const rows = page.locator('tbody tr')
|
||||
await expect(rows).toHaveCount(20)
|
||||
})
|
||||
|
||||
test('renders formatted distance values in km', async ({ page }) => {
|
||||
// Activity 1: distanceM = 10000 → "10.0 km"
|
||||
await expect(page.getByRole('cell', { name: '10.0 km' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('renders HR in red for activities with heartrate', async ({ page }) => {
|
||||
// Activity index 1 has averageHeartrateBpm = 141 (i=1, not divisible by 4)
|
||||
const hrCell = page.locator('td span.text-red-500').first()
|
||||
await expect(hrCell).toBeVisible()
|
||||
await expect(hrCell).toContainText('bpm')
|
||||
})
|
||||
|
||||
test('renders dash for activities without HR data', async ({ page }) => {
|
||||
// Activity index 0 has averageHeartrateBpm = 0 → shows "—"
|
||||
// Sort by DATE ascending to get activity 0 on last page (it's oldest)
|
||||
// Just check that at least one dash cell exists
|
||||
const dashCells = page.locator('td span.text-zinc-300')
|
||||
await expect(dashCells.first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('renders power in bold for Ride activities', async ({ page }) => {
|
||||
// Rides (i % 3 === 1) have averagePowerWatt > 0
|
||||
const powerCell = page.locator('td span.font-semibold.text-zinc-800').first()
|
||||
await expect(powerCell).toBeVisible()
|
||||
await expect(powerCell).toContainText('W')
|
||||
})
|
||||
|
||||
test('renders TSS with gradient text', async ({ page }) => {
|
||||
const tssCells = page.locator('td span.gradient-text')
|
||||
await expect(tssCells.first()).toBeVisible()
|
||||
})
|
||||
|
||||
// ── Search / filter ───────────────────────────────────────────────────────────
|
||||
|
||||
test('search input is visible with placeholder', async ({ page }) => {
|
||||
await expect(page.getByPlaceholder('Search activities...')).toBeVisible()
|
||||
})
|
||||
|
||||
test('search filters rows by activity name', async ({ page }) => {
|
||||
await page.getByPlaceholder('Search activities...').fill('Morning Run')
|
||||
// 25 activities, 9 are "Morning Run" (i=0,3,6,9,12,15,18,21,24 → i%3===0)
|
||||
const rows = page.locator('tbody tr')
|
||||
const count = await rows.count()
|
||||
expect(count).toBeLessThan(20)
|
||||
expect(count).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('search filters by activity type', async ({ page }) => {
|
||||
await page.getByPlaceholder('Search activities...').fill('Swim')
|
||||
const rows = page.locator('tbody tr')
|
||||
const count = await rows.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
// All visible rows should contain "Swim" somewhere
|
||||
const allText = await page.locator('tbody').textContent()
|
||||
expect(allText).toContain('Swim')
|
||||
})
|
||||
|
||||
test('empty search term restores all rows', async ({ page }) => {
|
||||
await page.getByPlaceholder('Search activities...').fill('Morning Run')
|
||||
await page.getByPlaceholder('Search activities...').clear()
|
||||
const rows = page.locator('tbody tr')
|
||||
await expect(rows).toHaveCount(20)
|
||||
})
|
||||
|
||||
test('search with no match shows 0 results in pagination', async ({ page }) => {
|
||||
await page.getByPlaceholder('Search activities...').fill('xyznotexist')
|
||||
await expect(page.getByText('0 results')).toBeVisible()
|
||||
await expect(page.locator('tbody tr')).toHaveCount(0)
|
||||
})
|
||||
|
||||
// ── Pagination ────────────────────────────────────────────────────────────────
|
||||
|
||||
test('pagination shows correct result count and page info', async ({ page }) => {
|
||||
// 25 activities total, page size 20 → "25 results · page 1 of 2"
|
||||
await expect(page.getByText(`${MOCK_ACTIVITIES.length} results`)).toBeVisible()
|
||||
await expect(page.getByText('page 1 of 2')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Prev button is disabled on first page', async ({ page }) => {
|
||||
const prev = page.getByRole('button', { name: /prev/i }).or(
|
||||
page.locator('button:has([data-testid="ChevronLeftIcon"]), button:has(svg)').first(),
|
||||
)
|
||||
// Use aria-disabled / disabled attribute on the first pagination button
|
||||
const buttons = page.locator('div.flex.items-center.gap-1 button')
|
||||
await expect(buttons.first()).toBeDisabled()
|
||||
})
|
||||
|
||||
test('Next button navigates to page 2', async ({ page }) => {
|
||||
const buttons = page.locator('div.flex.items-center.gap-1 button')
|
||||
await buttons.last().click()
|
||||
await expect(page.getByText('page 2 of 2')).toBeVisible()
|
||||
// Page 2 has the remaining 5 activities
|
||||
await expect(page.locator('tbody tr')).toHaveCount(5)
|
||||
})
|
||||
|
||||
test('Next button is disabled on last page', async ({ page }) => {
|
||||
const buttons = page.locator('div.flex.items-center.gap-1 button')
|
||||
await buttons.last().click() // go to page 2
|
||||
await expect(buttons.last()).toBeDisabled()
|
||||
})
|
||||
|
||||
test('Prev button navigates back to page 1', async ({ page }) => {
|
||||
const buttons = page.locator('div.flex.items-center.gap-1 button')
|
||||
await buttons.last().click() // go to page 2
|
||||
await buttons.first().click() // back to page 1
|
||||
await expect(page.getByText('page 1 of 2')).toBeVisible()
|
||||
await expect(page.locator('tbody tr')).toHaveCount(20)
|
||||
})
|
||||
|
||||
// ── Sorting ───────────────────────────────────────────────────────────────────
|
||||
|
||||
test('DATE column is sorted descending by default (most recent first)', async ({ page }) => {
|
||||
// Activity 1 is the most recent (1 day ago) — its name contains "1"
|
||||
const firstRow = page.locator('tbody tr').first()
|
||||
await expect(firstRow).toContainText('1')
|
||||
})
|
||||
|
||||
test('clicking DISTANCE header sorts by distance', async ({ page }) => {
|
||||
// TanStack Table v8 defaults numeric columns to desc-first
|
||||
// First click → descending (longest first = activity 24: 22000 m = 22.0 km)
|
||||
await page.locator('th:has-text("DISTANCE") button').click()
|
||||
await expect(page.locator('tbody tr').first()).toContainText('22.0 km')
|
||||
|
||||
// Second click → ascending (shortest first = activity 0: 10000 m = 10.0 km)
|
||||
await page.locator('th:has-text("DISTANCE") button').click()
|
||||
await expect(page.locator('tbody tr').first()).toContainText('10.0 km')
|
||||
})
|
||||
|
||||
// ── Navigation ────────────────────────────────────────────────────────────────
|
||||
|
||||
test('sidebar DASHBOARD link navigates back to Dashboard', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'DASHBOARD' }).click()
|
||||
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('sidebar placeholder pages show coming-soon text', async ({ page }) => {
|
||||
const items: Array<[string, string]> = [
|
||||
['TRAINING LOAD', 'Training Load'],
|
||||
['ANALYTICS', 'Analytics'],
|
||||
['CALENDAR', 'Calendar'],
|
||||
['SETTINGS', 'Settings'],
|
||||
]
|
||||
for (const [navLabel, placeholderName] of items) {
|
||||
await page.getByRole('button', { name: navLabel }).click()
|
||||
await expect(page.getByText('Coming Soon')).toBeVisible()
|
||||
await expect(page.getByRole('heading', { name: placeholderName })).toBeVisible()
|
||||
}
|
||||
})
|
||||
71
peakup-frontend/e2e/dashboard.spec.ts
Normal file
71
peakup-frontend/e2e/dashboard.spec.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { setupApiMocks, login } from './fixtures'
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupApiMocks(page)
|
||||
await login(page)
|
||||
})
|
||||
|
||||
test('renders page header', async ({ page }) => {
|
||||
await expect(page.getByText('Overview')).toBeVisible()
|
||||
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('renders Weekly Goals section with 4 cards', async ({ page }) => {
|
||||
await expect(page.getByText('This Week')).toBeVisible()
|
||||
await expect(page.getByText('Distance')).toBeVisible()
|
||||
await expect(page.getByText('Time')).toBeVisible()
|
||||
// TSS label appears in both the weekly goal card and the chart header — use first()
|
||||
await expect(page.getByText('TSS').first()).toBeVisible()
|
||||
await expect(page.getByText('Elevation')).toBeVisible()
|
||||
})
|
||||
|
||||
test('weekly goal cards show target values', async ({ page }) => {
|
||||
await expect(page.getByText('/ 100 km')).toBeVisible()
|
||||
await expect(page.getByText('/ 10 h')).toBeVisible()
|
||||
await expect(page.getByText('/ 500 pts')).toBeVisible()
|
||||
await expect(page.getByText('/ 3000 m')).toBeVisible()
|
||||
})
|
||||
|
||||
test('renders Fitness State section with CTL / ATL / TSB cards', async ({ page }) => {
|
||||
await expect(page.getByText('Fitness State')).toBeVisible()
|
||||
await expect(page.getByText('Fitness (CTL)')).toBeVisible()
|
||||
await expect(page.getByText('Fatigue (ATL)')).toBeVisible()
|
||||
await expect(page.getByText('Form (TSB)')).toBeVisible()
|
||||
await expect(page.getByText('Chronic Training Load')).toBeVisible()
|
||||
await expect(page.getByText('Acute Training Load')).toBeVisible()
|
||||
})
|
||||
|
||||
test('renders Weekly Training Load chart with SVG', async ({ page }) => {
|
||||
await expect(page.getByText('Weekly Training Load (TSS)')).toBeVisible()
|
||||
const svg = page.locator('text=Weekly Training Load (TSS)').locator('~ *').first()
|
||||
// Recharts renders an svg inside the ResponsiveContainer — just assert the section exists
|
||||
await expect(page.locator('svg').first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('renders Recent Activities section with activity rows', async ({ page }) => {
|
||||
await expect(page.getByText('Recent Activities')).toBeVisible()
|
||||
// There should be 5 recent activity names visible (most recent from MOCK_ACTIVITIES)
|
||||
// Activity names follow pattern "Morning Run 1", "Cycling Session 2", "Swim 3" etc.
|
||||
const rows = page.locator('text=Morning Run, text=Cycling Session, text=Swim').first()
|
||||
// At least 5 activity names should appear
|
||||
const activityNames = page.locator('p.text-sm.font-medium')
|
||||
await expect(activityNames).toHaveCount(5)
|
||||
})
|
||||
|
||||
test('shows distance and duration in recent activities', async ({ page }) => {
|
||||
// formatDistance(10000 + 0 * 500) = "10.0 km" for activity 1
|
||||
await expect(page.getByText('10.0 km')).toBeVisible()
|
||||
})
|
||||
|
||||
test('shows "No activities yet" when activity list is empty', async ({ page }) => {
|
||||
// Override the activities mock to return empty array
|
||||
await page.route('**/api/activities?*', (route) => route.fulfill({ json: [] }))
|
||||
await page.reload()
|
||||
// After reload, login dialog appears again — log in
|
||||
await page.getByPlaceholder('your login').fill('testuser')
|
||||
await page.getByRole('button', { name: 'Sign in' }).click()
|
||||
await page.getByRole('button', { name: 'DASHBOARD' }).waitFor()
|
||||
await expect(page.getByText('No activities yet')).toBeVisible()
|
||||
await expect(page.getByText('No training data yet')).toBeVisible()
|
||||
})
|
||||
83
peakup-frontend/e2e/fixtures.ts
Normal file
83
peakup-frontend/e2e/fixtures.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { type Page } from '@playwright/test'
|
||||
|
||||
// ── Mock athlete ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const MOCK_ATHLETE = {
|
||||
id: 1,
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
login: 'testuser',
|
||||
city: 'Berlin',
|
||||
country: 'Germany',
|
||||
sex: 'M',
|
||||
weight: 70,
|
||||
}
|
||||
|
||||
// ── Mock activities ───────────────────────────────────────────────────────────
|
||||
// 25 activities spread over the last 12 weeks so the chart has data.
|
||||
// Mix of ride/run/swim types; some with HR/power, some without.
|
||||
|
||||
function daysAgo(n: number): string {
|
||||
const d = new Date()
|
||||
d.setDate(d.getDate() - n)
|
||||
return d.toISOString()
|
||||
}
|
||||
|
||||
export const MOCK_ACTIVITIES = Array.from({ length: 25 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
athleteId: 1,
|
||||
originId: `origin-${i + 1}`,
|
||||
name: i % 3 === 0 ? `Morning Run ${i + 1}` : i % 3 === 1 ? `Cycling Session ${i + 1}` : `Swim ${i + 1}`,
|
||||
type: i % 3 === 0 ? 'Run' : i % 3 === 1 ? 'Ride' : 'Swim',
|
||||
sportType: i % 3 === 0 ? 'Run' : i % 3 === 1 ? 'Ride' : 'Swim',
|
||||
startTime: daysAgo(i * 3 + 1), // spread ~75 days back (within 12 weeks)
|
||||
timezone: 'Europe/Berlin',
|
||||
distanceM: 10_000 + i * 500, // 10–22 km
|
||||
movingTimeS: 3_600 + i * 120, // 1h–1h50
|
||||
elapsedTimeS: 3_700 + i * 120,
|
||||
averageSpeedMps: 3.5 + i * 0.05,
|
||||
maxSpeedMps: 5.0 + i * 0.05,
|
||||
averageHeartrateBpm: i % 4 === 0 ? 0 : 140 + (i % 20),
|
||||
maxHeartrateBPM: i % 4 === 0 ? 0 : 170 + (i % 15),
|
||||
averagePowerWatt: i % 3 === 1 ? 200 + (i % 50) : 0,
|
||||
normalizedPowerWatt: i % 3 === 1 ? 210 + (i % 50) : 0,
|
||||
totalWorkKJ: i % 3 === 1 ? 800 + i * 10 : 0,
|
||||
totalElevationGainM: i % 2 === 0 ? 200 + i * 10 : 0,
|
||||
trainingLoad: 60 + (i % 80),
|
||||
intensityFactor: 0.75 + (i % 10) * 0.01,
|
||||
ftp: 250,
|
||||
ctl: 55 + (i % 20),
|
||||
atl: 65 + (i % 25),
|
||||
averageCadenceRPM: 85 + (i % 10),
|
||||
calories: 600 + i * 20,
|
||||
streamStatus: 'COMPLETE',
|
||||
}))
|
||||
|
||||
// ── API mock setup ────────────────────────────────────────────────────────────
|
||||
|
||||
export async function setupApiMocks(page: Page) {
|
||||
// Athlete by login
|
||||
await page.route('**/api/athletes/by-login/testuser', (route) =>
|
||||
route.fulfill({ json: MOCK_ATHLETE }),
|
||||
)
|
||||
|
||||
// Unknown login → 404
|
||||
await page.route('**/api/athletes/by-login/unknown', (route) =>
|
||||
route.fulfill({ status: 404, body: 'Not Found' }),
|
||||
)
|
||||
|
||||
// Activities list
|
||||
await page.route('**/api/activities?*', (route) =>
|
||||
route.fulfill({ json: MOCK_ACTIVITIES }),
|
||||
)
|
||||
}
|
||||
|
||||
// ── Login helper ──────────────────────────────────────────────────────────────
|
||||
|
||||
export async function login(page: Page, username = 'testuser') {
|
||||
await page.goto('/')
|
||||
await page.getByPlaceholder('your login').fill(username)
|
||||
await page.getByRole('button', { name: 'Sign in' }).click()
|
||||
// Wait until sidebar is visible (login succeeded)
|
||||
await page.getByRole('button', { name: 'DASHBOARD' }).waitFor()
|
||||
}
|
||||
43
peakup-frontend/e2e/login.spec.ts
Normal file
43
peakup-frontend/e2e/login.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { setupApiMocks } from './fixtures'
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupApiMocks(page)
|
||||
})
|
||||
|
||||
test('shows login dialog on first load', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await expect(page.getByPlaceholder('your login')).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible()
|
||||
await expect(page.getByText('Performance Analytics')).toBeVisible()
|
||||
})
|
||||
|
||||
test('sign-in button is disabled when input is empty', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await expect(page.getByRole('button', { name: 'Sign in' })).toBeDisabled()
|
||||
})
|
||||
|
||||
test('shows error message for unknown login', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.getByPlaceholder('your login').fill('unknown')
|
||||
await page.getByRole('button', { name: 'Sign in' }).click()
|
||||
await expect(page.getByText('Unknown athlete')).toBeVisible()
|
||||
})
|
||||
|
||||
test('error clears when user types again after failure', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.getByPlaceholder('your login').fill('unknown')
|
||||
await page.getByRole('button', { name: 'Sign in' }).click()
|
||||
await expect(page.getByText('Unknown athlete')).toBeVisible()
|
||||
await page.getByPlaceholder('your login').type('x')
|
||||
await expect(page.getByText('Unknown athlete')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('successful login shows sidebar and dashboard', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.getByPlaceholder('your login').fill('testuser')
|
||||
await page.getByRole('button', { name: 'Sign in' }).click()
|
||||
await expect(page.getByRole('button', { name: 'DASHBOARD' })).toBeVisible()
|
||||
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
|
||||
await expect(page.getByPlaceholder('your login')).not.toBeVisible()
|
||||
})
|
||||
64
peakup-frontend/package-lock.json
generated
64
peakup-frontend/package-lock.json
generated
@@ -22,6 +22,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@types/node": "^24.12.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
@@ -586,6 +587,22 @@
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||
@@ -3137,6 +3154,53 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
@@ -24,6 +26,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@types/node": "^24.12.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
|
||||
29
peakup-frontend/playwright.config.ts
Normal file
29
peakup-frontend/playwright.config.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: 'html',
|
||||
|
||||
use: {
|
||||
baseURL: 'http://localhost:5173',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: 'http://localhost:5173',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 30_000,
|
||||
},
|
||||
})
|
||||
4
peakup-frontend/test-results/.last-run.json
Normal file
4
peakup-frontend/test-results/.last-run.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"failedTests": []
|
||||
}
|
||||
Reference in New Issue
Block a user