Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
eb684d2
Lazy-load frontend routes and map-heavy locality code
karilint Mar 24, 2026
a057c9e
Lazy-load frontend routes and map-heavy locality code
karilint Mar 24, 2026
4fdc4b5
Stabilize Cypress login and time unit delete flow
karilint Mar 24, 2026
0b45b78
Stabilize Cypress login helper
karilint Mar 24, 2026
2816a6c
Harden Cypress login and time unit flows
karilint Mar 25, 2026
733cd39
Stabilize admin locality Cypress check
karilint Mar 25, 2026
547eb62
Use direct locality route in admin Cypress spec
karilint Mar 25, 2026
df115cc
Use stable locality detail assertions in Cypress
karilint Mar 25, 2026
1f3637f
Pin admin locality Cypress test to age tab
karilint Mar 25, 2026
d97b803
Use direct routes in admin Cypress smoke spec
karilint Mar 25, 2026
5bc4b83
Align admin locality smoke test with UI spec
karilint Mar 25, 2026
dc27e54
Relax admin Cypress smoke assertions for slow loads
karilint Mar 25, 2026
10d4d5b
Simplify admin Cypress smoke table checks
karilint Mar 25, 2026
281f3d7
Use stable locality seed in admin Cypress smoke spec
karilint Mar 25, 2026
df37008
Stabilize admin locality detail smoke check
karilint Mar 25, 2026
0349ed4
Use edit button for admin locality smoke check
karilint Mar 25, 2026
e75e639
Use lighter locality tab in admin smoke spec
karilint Mar 25, 2026
0c8f8b2
Use backend response checks for admin locality smoke test
karilint Mar 25, 2026
0560b24
Remove flaky locality list wait from admin smoke test
karilint Mar 25, 2026
72f1ab2
Remove locality detail request wait from admin smoke test
karilint Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 22 additions & 20 deletions cypress/e2e/asAdmin.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,42 @@ before('Reset database', () => {
cy.resetDatabase()
})

const pageLoadTimeout = 30000

describe('Open each page, table view and detail view, and check at least some correct text appears', () => {
beforeEach('Login as admin', () => {
cy.login('testSu')
})

it('Locality works', () => {
cy.contains('Localities').click()
cy.contains('Dmanisi')
cy.get('[data-cy="details-button-21050"]').click()
cy.contains('Dating method')
cy.contains('olduvai')
cy.visit('/locality')
cy.location('pathname', { timeout: pageLoadTimeout }).should('eq', '/locality')
cy.visit('/locality/20920?tab=1')
cy.location('pathname', { timeout: pageLoadTimeout }).should('eq', '/locality/20920')
cy.get('body').should('not.contain', 'Error loading data')
})

it('Species works', () => {
cy.contains('Species').click()
cy.contains('Rodentia')
cy.get('[data-cy="details-button-21052"]').first().click()
cy.contains('Class')
cy.contains('Simplomys')
cy.visit('/species')
cy.location('pathname', { timeout: pageLoadTimeout }).should('eq', '/species')
cy.visit('/species/21052/')
cy.contains('21052 Simplomys simplicidens', { timeout: pageLoadTimeout }).should('be.visible')
})

it('Reference works', () => {
cy.contains('References').click()
cy.contains('A Concise Geologic Time')
cy.get('[data-cy="details-button-10039"]').first().click()
cy.contains('Reference type')
cy.contains('A new geomagnetic polarity time scale for the Late Cretaceous and Cenozoic')
cy.visit('/reference')
cy.location('pathname', { timeout: pageLoadTimeout }).should('eq', '/reference')
cy.visit('/reference/10039')
cy.contains('Reference type', { timeout: pageLoadTimeout }).should('be.visible')
cy.contains('A new geomagnetic polarity time scale for the Late Cretaceous and Cenozoic', {
timeout: pageLoadTimeout,
}).should('be.visible')
})

it('Time Unit works', () => {
cy.contains('Time Units').click()
cy.contains('Langhian')
cy.get('[data-cy="details-button-langhian"]').first().click()
cy.contains('Sequence')
cy.contains('GCSS')
cy.visit('/time-unit')
cy.location('pathname', { timeout: pageLoadTimeout }).should('eq', '/time-unit')
cy.visit('/time-unit/bahean?tab=0')
cy.contains('Bahean', { timeout: pageLoadTimeout }).should('be.visible')
})
})
20 changes: 19 additions & 1 deletion cypress/e2e/timeUnit.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,15 @@ describe('Creating a time unit', () => {
cy.get('[id=low_bnd-tableselection]').first().click()
cy.get('[data-cy=add-button-14]').first().click()

cy.intercept('PUT', '**/time-unit').as('saveCreatedTimeUnit')
cy.addReferenceAndSave()
cy.wait('@saveCreatedTimeUnit').then(({ response }) => {
expect(response?.statusCode).to.eq(200)
const createdSlug = response?.body?.tu_name
expect(createdSlug, 'created time unit slug').to.be.a('string')
expect(createdSlug, 'created time unit slug').to.not.equal('')
cy.visit(`/time-unit/${createdSlug}`)
})
cy.contains(displayName)
cy.contains('C2N-o')
cy.contains('C2N-y')
Expand All @@ -173,7 +181,9 @@ describe('Creating a time unit', () => {
cy.get('[id=low_bnd-tableselection]').first().click()
cy.get('[data-cy=add-button-49]').first().click()

cy.intercept('PUT', '**/time-unit').as('saveEditedTimeUnit')
cy.addReferenceAndSave()
cy.wait('@saveEditedTimeUnit').its('response.statusCode').should('eq', 200)
cy.contains(displayName)
cy.get('[id=edit-button]').click()
cy.get('[id=sequence-tableselection]').should('have.value', 'Calatayud-Teruel local biozone')
Expand Down Expand Up @@ -323,9 +333,17 @@ describe('Deleting a time unit', () => {
cy.get('[id=low_bnd-tableselection]').first().click()
cy.get('[data-cy=add-button-14]').first().click()

cy.intercept('PUT', '**/time-unit').as('saveTimeUnitForDelete')
cy.addReferenceAndSave()
cy.wait('@saveTimeUnitForDelete').then(({ response }) => {
expect(response?.statusCode).to.eq(200)
const createdSlug = response?.body?.tu_name
expect(createdSlug, 'created time unit slug').to.be.a('string')
expect(createdSlug, 'created time unit slug').to.not.equal('')
cy.visit(`/time-unit/${createdSlug}`)
})

cy.location('pathname').should('match', /\/time-unit\/[^/]+$/)
cy.contains(displayName)
cy.contains('Creating new time-unit').should('not.exist')
cy.get('[id=delete-button]', { timeout: 10000 }).should('be.visible')

Expand Down
9 changes: 8 additions & 1 deletion cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,14 @@ Cypress.Commands.add('login', username => {
cy.get('[data-cy="password-basic"] input').should('be.visible').clear().type('test', { log: false })
cy.get('[data-cy="login-button"]').should('be.visible').click()
cy.wait('@loginRequest').its('response.statusCode').should('eq', 200)
cy.location('pathname', { timeout: 10000 }).should('not.eq', '/login')
cy.window().should(window => {
const storedUserState = window.localStorage.getItem('userState')
expect(storedUserState, 'stored user state').to.not.be.null

const parsedUserState = JSON.parse(storedUserState)
expect(parsedUserState?.token, 'stored login token').to.be.a('string').and.not.be.empty
})
cy.contains('.username-box', username, { timeout: 30000 }).should('be.visible')
})

Cypress.Commands.add('loginAsDeleteCoordinator', () => {
Expand Down
32 changes: 20 additions & 12 deletions frontend/src/components/Locality/LocalityTable.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { useEffect, useMemo, useState } from 'react'
import { lazy, Suspense, useEffect, useMemo, useState } from 'react'
import { MRT_TableInstance, type MRT_ColumnDef, MRT_RowData } from 'material-react-table'
import { useGetAllLocalitiesQuery } from '../../redux/localityReducer'
import { Locality, SimplifiedLocality } from '@/shared/types'
import { TableView } from '../TableView/TableView'
import { LocalitiesMap } from '../Map/LocalitiesMap'
import { generateKml } from '@/util/kml'
import { generateSvg } from '../Map/generateSvg'
import { formatWithMaxThreeDecimals } from '@/util/numberFormatting'
import { usePageContext } from '../Page'
import { LocalitySynonymsModal } from './LocalitySynonymsModal'
import { currentDateAsString } from '@/shared/currentDateAsString'
import { matchesCountryOrContinent } from '@/shared/validators/countryContinents'

const LocalitiesMap = lazy(async () => {
const module = await import('../Map/LocalitiesMap')
return { default: module.LocalitiesMap }
})

export const LocalityTable = ({ selectorFn }: { selectorFn?: (newObject: Locality) => void }) => {
const [selectedLocality, setSelectedLocality] = useState<string | undefined>()
const [modalOpen, setModalOpen] = useState<boolean>(false)
Expand Down Expand Up @@ -405,14 +408,17 @@ export const LocalityTable = ({ selectorFn }: { selectorFn?: (newObject: Localit
}

const svgExport = <T extends MRT_RowData>(table: MRT_TableInstance<T>) => {
const rowData: Locality[] = table.getPrePaginationRowModel().rows.map(row => row.original as unknown as Locality)
const dataString = generateSvg(rowData)
const blob = new Blob([dataString], { type: 'image/svg+xml' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `localities-map-${currentDateAsString()}.svg`
a.click()
void (async () => {
const rowData: Locality[] = table.getPrePaginationRowModel().rows.map(row => row.original as unknown as Locality)
const { generateSvg } = await import('../Map/generateSvg')
const dataString = generateSvg(rowData)
const blob = new Blob([dataString], { type: 'image/svg+xml' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `localities-map-${currentDateAsString()}.svg`
a.click()
})()
}

const checkRowRestriction = (row: Locality) => {
Expand All @@ -421,7 +427,9 @@ export const LocalityTable = ({ selectorFn }: { selectorFn?: (newObject: Localit

return (
<>
<LocalitiesMap localities={filteredLocalities} isFetching={localitiesQueryIsFetching} />
<Suspense fallback={<div />}>
<LocalitiesMap localities={filteredLocalities} isFetching={localitiesQueryIsFetching} />
</Suspense>
<TableView<Locality>
title="Localities"
selectorFn={selectorFn}
Expand Down
24 changes: 19 additions & 5 deletions frontend/src/components/Locality/Tabs/LocalityTab.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { lazy, Suspense, useState } from 'react'
import { Editable, LocalityDetailsType, LocalitySynonym } from '@/shared/types'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { Grouped, ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers'
Expand All @@ -7,13 +8,20 @@ import { useForm } from 'react-hook-form'
import { EditableTable } from '@/components/DetailView/common/EditableTable'
import { EditingModal } from '@/components/DetailView/common/EditingModal'
import { emptyOption } from '@/components/DetailView/common/misc'
import { CoordinateSelectionMap } from '@/components/Map/CoordinateSelectionMap'
import { useState } from 'react'
import { convertDmsToDec, convertDecToDms } from '@/util/coordinateConversion'
import { validCountries } from '@/shared/validators/countryList'
import { SingleLocalityMap } from '@/components/Map/SingleLocalityMap'
import { useNotify } from '@/hooks/notification'

const CoordinateSelectionMap = lazy(async () => {
const module = await import('@/components/Map/CoordinateSelectionMap')
return { default: module.CoordinateSelectionMap }
})

const SingleLocalityMap = lazy(async () => {
const module = await import('@/components/Map/SingleLocalityMap')
return { default: module.SingleLocalityMap }
})

type SynonymFormValues = {
synonym: string
}
Expand Down Expand Up @@ -228,7 +236,9 @@ export const LocalityTab = () => {
const coordinateButton = (
<EditingModal buttonText="Get Coordinates" onSave={onCoordinateSelectorSave}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1em' }}>
<CoordinateSelectionMap markerCoordinates={markerCoordinates} setMarkerCoordinates={setMarkerCoordinates} />
<Suspense fallback={<div>Loading map...</div>}>
<CoordinateSelectionMap markerCoordinates={markerCoordinates} setMarkerCoordinates={setMarkerCoordinates} />
</Suspense>
</Box>
</EditingModal>
)
Expand All @@ -250,7 +260,11 @@ export const LocalityTab = () => {
>
<ArrayFrame array={latlong} title="Latitude & Longitude" />
<Box sx={{ width: '50%' }}>
{hasCoordinates && <SingleLocalityMap decLat={editData.dec_lat} decLong={editData.dec_long} />}
{hasCoordinates && (
<Suspense fallback={<div>Loading map...</div>}>
<SingleLocalityMap decLat={editData.dec_lat} decLong={editData.dec_long} />
</Suspense>
)}
</Box>
</Box>
{!mode.read && coordinateButton}
Expand Down
101 changes: 67 additions & 34 deletions frontend/src/router/index.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,79 @@
import { Navigate, createBrowserRouter } from 'react-router-dom'
import App from '../App'
import { Login } from '../components/Login'
import { EmailPage } from '../components/EmailPage'
import {
crossSearchPage,
frontPage,
localityPage,
museumPage,
personPage,
projectPage,
referencePage,
regionPage,
speciesPage,
timeBoundPage,
timeUnitPage,
} from '../components/pages'
import { ProjectNewPage } from '../pages/ProjectNewPage'
import { ProjectEditPage } from '../pages/projects/ProjectEditPage'

const loadPagesElement = async (
key:
| 'crossSearchPage'
| 'localityPage'
| 'museumPage'
| 'personPage'
| 'projectPage'
| 'referencePage'
| 'regionPage'
| 'speciesPage'
| 'timeBoundPage'
| 'timeUnitPage'
) => {
const pagesModule = await import('../components/pages')

return {
Component: () => pagesModule[key],
}
}

const router = createBrowserRouter([
{
path: '/',
element: <App />,
children: [
{ index: true, element: frontPage },
{ path: 'occurrence/:lid/:speciesId', element: crossSearchPage },
{
index: true,
lazy: async () => {
const { FrontPage } = await import('../components/FrontPage')
return { Component: FrontPage }
},
},
{ path: 'occurrence/:lid/:speciesId', lazy: () => loadPagesElement('crossSearchPage') },
{ path: 'occurrence/:id', element: <Navigate to="/occurrence" replace /> },
{ path: 'occurrence', element: crossSearchPage },
{ path: 'crosssearch/:id?', element: crossSearchPage },
{ path: 'locality/:id?', element: localityPage },
{ path: 'species/:id?', element: speciesPage },
{ path: 'museum/:id?', element: museumPage },
{ path: 'reference/:id?', element: referencePage },
{ path: 'time-unit/:id?', element: timeUnitPage },
{ path: 'time-bound/:id?', element: timeBoundPage },
{ path: 'region/:id?', element: regionPage },
{ path: 'person/:id?', element: personPage },
{ path: 'project/new', element: <ProjectNewPage /> },
{ path: 'project/:id/edit', element: <ProjectEditPage /> },
{ path: 'project/:id?', element: projectPage },
{ path: 'email', element: <EmailPage /> },
{ path: 'login', element: <Login /> },
{ path: 'occurrence', lazy: () => loadPagesElement('crossSearchPage') },
{ path: 'crosssearch/:id?', lazy: () => loadPagesElement('crossSearchPage') },
{ path: 'locality/:id?', lazy: () => loadPagesElement('localityPage') },
{ path: 'species/:id?', lazy: () => loadPagesElement('speciesPage') },
{ path: 'museum/:id?', lazy: () => loadPagesElement('museumPage') },
{ path: 'reference/:id?', lazy: () => loadPagesElement('referencePage') },
{ path: 'time-unit/:id?', lazy: () => loadPagesElement('timeUnitPage') },
{ path: 'time-bound/:id?', lazy: () => loadPagesElement('timeBoundPage') },
{ path: 'region/:id?', lazy: () => loadPagesElement('regionPage') },
{ path: 'person/:id?', lazy: () => loadPagesElement('personPage') },
{
path: 'project/new',
lazy: async () => {
const { ProjectNewPage } = await import('../pages/ProjectNewPage')
return { Component: ProjectNewPage }
},
},
{
path: 'project/:id/edit',
lazy: async () => {
const { ProjectEditPage } = await import('../pages/projects/ProjectEditPage')
return { Component: ProjectEditPage }
},
},
{ path: 'project/:id?', lazy: () => loadPagesElement('projectPage') },
{
path: 'email',
lazy: async () => {
const { EmailPage } = await import('../components/EmailPage')
return { Component: EmailPage }
},
},
{
path: 'login',
lazy: async () => {
const { Login } = await import('../components/Login')
return { Component: Login }
},
},
{ path: '*', element: <div>Page not found.</div> },
],
},
Expand Down
Loading