diff --git a/frontend/src/components/NavBar.test.tsx b/frontend/src/components/NavBar.test.tsx
new file mode 100644
index 00000000..42087988
--- /dev/null
+++ b/frontend/src/components/NavBar.test.tsx
@@ -0,0 +1,51 @@
+import { describe, expect, it, jest } from '@jest/globals'
+import { render, screen } from '@testing-library/react'
+import { MemoryRouter } from 'react-router-dom'
+import { NavBar } from './NavBar'
+import { occurrenceLabels } from '@/constants/occurrenceLabels'
+import { Role } from '@/shared/types'
+
+jest.mock('react-redux', () => ({
+ useDispatch: () => jest.fn(),
+}))
+
+jest.mock('../redux/userReducer', () => ({
+ clearUser: () => ({ type: 'user/clearUser' }),
+}))
+
+jest.mock('../redux/api', () => ({
+ api: {
+ util: {
+ resetApiState: () => ({ type: 'api/resetApiState' }),
+ },
+ },
+}))
+
+jest.mock('@/hooks/user', () => ({
+ useUser: () => ({
+ token: null,
+ username: null,
+ role: Role.ReadOnly,
+ initials: null,
+ localities: [],
+ isFirstLogin: undefined,
+ }),
+}))
+
+jest.mock('@/hooks/notification', () => ({
+ useNotify: () => ({ notify: jest.fn() }),
+}))
+
+jest.mock('../resource/nowlogo.jpg', () => 'now-logo')
+
+describe('NavBar', () => {
+ it('shows Occurrences navigation label', () => {
+ render(
+
+
+
+ )
+
+ expect(screen.getByText(occurrenceLabels.plural)).toBeTruthy()
+ })
+})
diff --git a/frontend/src/components/NavBar.tsx b/frontend/src/components/NavBar.tsx
index c72f596e..e23a6e16 100755
--- a/frontend/src/components/NavBar.tsx
+++ b/frontend/src/components/NavBar.tsx
@@ -9,6 +9,7 @@ import { useNotify } from '@/hooks/notification'
import PersonIcon from '@mui/icons-material/Person'
import '../styles/NavBar.css'
import { LinkDefinition, NavBarLink } from './NavBarLink'
+import { occurrenceLabels } from '@/constants/occurrenceLabels'
import logo from '../resource/nowlogo.jpg'
@@ -20,7 +21,7 @@ export const NavBar = () => {
const pages: LinkDefinition[] = [
{ title: 'Localities', url: '/locality' },
{ title: 'Species', url: '/species' },
- { title: 'Locality-Species', url: '/crosssearch' },
+ { title: occurrenceLabels.plural, url: '/occurrence' },
{ title: 'References', url: '/reference' },
{ title: 'Time Units', url: '/time-unit' },
{ title: 'Time Bounds', url: '/time-bound', allowedRoles: [Role.Admin, Role.EditUnrestricted] },
diff --git a/frontend/src/components/Species/SpeciesDetails.tsx b/frontend/src/components/Species/SpeciesDetails.tsx
index c6ad2ba2..129f8f64 100755
--- a/frontend/src/components/Species/SpeciesDetails.tsx
+++ b/frontend/src/components/Species/SpeciesDetails.tsx
@@ -17,6 +17,7 @@ import { emptySpecies } from '../DetailView/common/defaultValues'
import { useNotify } from '@/hooks/notification'
import { useEffect, useState } from 'react'
import { fixNullValuesInTaxonomyFields } from '@/util/taxonomyUtilities'
+import { occurrenceLabels } from '@/constants/occurrenceLabels'
export const SpeciesDetails = ({
wrapWithUnsavedChangesProvider = true,
@@ -105,7 +106,7 @@ export const SpeciesDetails = ({
content:
,
},
{
- title: 'Locality Species',
+ title: occurrenceLabels.plural,
content:
,
},
{
diff --git a/frontend/src/components/Species/Tabs/LocalitySpeciesTab.tsx b/frontend/src/components/Species/Tabs/LocalitySpeciesTab.tsx
index 8ff729f3..a77d1c77 100755
--- a/frontend/src/components/Species/Tabs/LocalitySpeciesTab.tsx
+++ b/frontend/src/components/Species/Tabs/LocalitySpeciesTab.tsx
@@ -11,6 +11,7 @@ import { calculateNormalizedMesowearScore } from '@/shared/utils/mesowear'
import { applyDefaultSpeciesOrdering, hasActiveSortingInSearch } from '@/components/DetailView/common/DetailTabTable'
import { useLocation } from 'react-router-dom'
import { useMemo } from 'react'
+import { occurrenceLabels } from '@/constants/occurrenceLabels'
const hasMesowearScoreInputs = (row: SpeciesLocality) => {
return (
@@ -197,7 +198,7 @@ export const LocalitySpeciesTab = () => {
}
const editingModal = (
-
+
@@ -212,7 +213,7 @@ export const LocalitySpeciesTab = () => {
)
return (
-
+
{!mode.read && editingModal}
, SpeciesDetailsType>
columns={columns}
diff --git a/frontend/src/components/Species/Tabs/__tests__/LocalitySpeciesTab.test.tsx b/frontend/src/components/Species/Tabs/__tests__/LocalitySpeciesTab.test.tsx
index f01f5d4c..ad36eede 100644
--- a/frontend/src/components/Species/Tabs/__tests__/LocalitySpeciesTab.test.tsx
+++ b/frontend/src/components/Species/Tabs/__tests__/LocalitySpeciesTab.test.tsx
@@ -5,6 +5,7 @@ import type { MRT_ColumnDef, MRT_Row } from 'material-react-table'
import { LocalitySpeciesTab } from '@/components/Species/Tabs/LocalitySpeciesTab'
import { modeOptionToMode, useDetailContext } from '@/components/DetailView/Context/DetailContext'
import type { SpeciesLocality } from '@/shared/types'
+import { occurrenceLabels } from '@/constants/occurrenceLabels'
jest.mock('@/components/DetailView/Context/DetailContext', () => ({
useDetailContext: jest.fn(),
@@ -31,11 +32,19 @@ jest.mock('@/components/DetailView/common/EditableTable', () => ({
}))
jest.mock('@/components/DetailView/common/EditingModal', () => ({
- EditingModal: ({ children }: { children: ReactNode }) => {children}
,
+ EditingModal: ({ children, buttonText }: { children: ReactNode; buttonText: string }) => (
+
+ {children}
+
+ ),
}))
jest.mock('@/components/DetailView/common/tabLayoutHelpers', () => ({
- Grouped: ({ children }: { children: ReactNode }) => {children}
,
+ Grouped: ({ children, title }: { children: ReactNode; title: string }) => (
+
+ {children}
+
+ ),
}))
const mockUseDetailContext = useDetailContext as jest.MockedFunction
@@ -65,6 +74,14 @@ describe('LocalitySpeciesTab MW Score rendering', () => {
expect(editableTableProps?.enableAdvancedTableControls).toBe(true)
})
+ it('uses occurrence terminology in modal button and group heading', () => {
+ const grouped = document.querySelector('[data-testid="grouped"]')
+ const editingModal = document.querySelector('[data-testid="editing-modal"]')
+
+ expect(grouped?.getAttribute('data-title')).toBe(occurrenceLabels.informationSectionTitle)
+ expect(editingModal?.getAttribute('data-button-text')).toBe(occurrenceLabels.addNewButton)
+ })
+
it('renders normalized score with 2 decimals for valid inputs', () => {
const cellRenderer = getMwScoreCellRenderer()
expect(cellRenderer).toBeDefined()
diff --git a/frontend/src/components/TableView/TableView.test.tsx b/frontend/src/components/TableView/TableView.test.tsx
index c9052441..317e7298 100644
--- a/frontend/src/components/TableView/TableView.test.tsx
+++ b/frontend/src/components/TableView/TableView.test.tsx
@@ -110,7 +110,7 @@ describe('TableView table help integration', () => {
columns={[{ header: 'Name', accessorKey: 'name' }]}
visibleColumns={{ name: true }}
data={[{ id: '1', name: 'Alpha', full_count: 1 }]}
- url="crosssearch"
+ url="occurrence"
isFetching={false}
isCrossSearchTable
serverSidePagination
diff --git a/frontend/src/components/TableView/TableView.tsx b/frontend/src/components/TableView/TableView.tsx
index fb5c0e9a..8300e663 100755
--- a/frontend/src/components/TableView/TableView.tsx
+++ b/frontend/src/components/TableView/TableView.tsx
@@ -421,7 +421,7 @@ export const TableView = ({
tableName={title}
kmlExport={kmlExport}
svgExport={svgExport}
- showNewButton={editRights.new && !selectorFn && title != 'Locality-Species-Cross-Search'}
+ showNewButton={editRights.new && !selectorFn && !isCrossSearchTable}
isCrossSearchTable={isCrossSearchTable}
selectorFn={selectorFn}
hideLeftButtons={false}
diff --git a/frontend/src/components/pages.tsx b/frontend/src/components/pages.tsx
index 6c4747fe..b24d94df 100755
--- a/frontend/src/components/pages.tsx
+++ b/frontend/src/components/pages.tsx
@@ -64,7 +64,7 @@ export const crossSearchPage = (
}
detailView={}
- viewName="crosssearch"
+ viewName="occurrence"
idFieldName="lid"
createTitle={(loc: LocalityDetailsType) => `${loc.lid} ${loc.loc_name}, ${loc.country}`}
createSubtitle={(loc: LocalityDetailsType) =>
diff --git a/frontend/src/constants/occurrenceLabels.ts b/frontend/src/constants/occurrenceLabels.ts
new file mode 100644
index 00000000..522f87dc
--- /dev/null
+++ b/frontend/src/constants/occurrenceLabels.ts
@@ -0,0 +1,7 @@
+export const occurrenceLabels = {
+ singular: 'Occurrence',
+ plural: 'Occurrences',
+ crossSearchTitle: 'Occurrences',
+ informationSectionTitle: 'Occurrences',
+ addNewButton: 'Add new Occurrence',
+} as const
diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx
index ba628a89..55d756a4 100644
--- a/frontend/src/router/index.tsx
+++ b/frontend/src/router/index.tsx
@@ -24,6 +24,7 @@ const router = createBrowserRouter([
element: ,
children: [
{ index: true, element: frontPage },
+ { path: 'occurrence/:id?', element: crossSearchPage },
{ path: 'crosssearch/:id?', element: crossSearchPage },
{ path: 'locality/:id?', element: localityPage },
{ path: 'species/:id?', element: speciesPage },
diff --git a/frontend/src/shared/types/data.ts b/frontend/src/shared/types/data.ts
index a6d0e01b..a05a4c87 100755
--- a/frontend/src/shared/types/data.ts
+++ b/frontend/src/shared/types/data.ts
@@ -24,7 +24,7 @@ export type LocalitySpeciesDetailsType = FixBigInt & { com_specie
export type SpeciesLocality = FixBigInt & {
now_loc: Prisma.now_loc
// Explicitly required in the Species locality payload because MW Score is
- // calculated client-side from these values in the Locality-Species table.
+ // calculated client-side from these values in the occurrence table.
mw_scale_min: number | null
mw_scale_max: number | null
mw_value: number | null
diff --git a/frontend/src/tests/README.md b/frontend/src/tests/README.md
index 6d3c0c7b..2a5e10f8 100644
--- a/frontend/src/tests/README.md
+++ b/frontend/src/tests/README.md
@@ -5,6 +5,6 @@
- Execute `npm run test -- --watch=false` from the `frontend/` directory to run all Jest specs in CI mode.
## Country or Continent filtering coverage
-- The Localities and Locality-Species tables now accept either a country name or a continent keyword in the "Country or Continent" column filter.
+- The Localities and Occurrence tables now accept either a country name or a continent keyword in the "Country or Continent" column filter.
- Automated coverage for the helper logic lives in `src/util/__tests__/countryContinents.test.ts`. Re-run that file directly with `npx jest src/util/__tests__/countryContinents.test.ts` if you need quick feedback while adjusting the mapping.
- When performing manual QA, confirm that entering a continent (for example, "Africa") narrows both tables to countries mapped to that continent and that standard country filters still work as expected.
diff --git a/frontend/src/tests/components/LocalitySpeciesTab.test.tsx b/frontend/src/tests/components/LocalitySpeciesTab.test.tsx
index 418d27f9..bd0404b6 100644
--- a/frontend/src/tests/components/LocalitySpeciesTab.test.tsx
+++ b/frontend/src/tests/components/LocalitySpeciesTab.test.tsx
@@ -5,6 +5,7 @@ import { LocalitySpeciesTab } from '@/components/Species/Tabs/LocalitySpeciesTab
import { modeOptionToMode, useDetailContext } from '@/components/DetailView/Context/DetailContext'
import type { SpeciesLocality } from '@/shared/types'
import type { MRT_ColumnDef, MRT_Row } from 'material-react-table'
+import { occurrenceLabels } from '@/constants/occurrenceLabels'
jest.mock('@/components/DetailView/Context/DetailContext', () => ({
useDetailContext: jest.fn(),
@@ -30,11 +31,19 @@ jest.mock('@/components/DetailView/common/EditableTable', () => ({
}))
jest.mock('@/components/DetailView/common/EditingModal', () => ({
- EditingModal: ({ children }: { children: ReactNode }) => {children}
,
+ EditingModal: ({ children, buttonText }: { children: ReactNode; buttonText: string }) => (
+
+ {children}
+
+ ),
}))
jest.mock('@/components/DetailView/common/tabLayoutHelpers', () => ({
- Grouped: ({ children }: { children: ReactNode }) => {children}
,
+ Grouped: ({ children, title }: { children: ReactNode; title: string }) => (
+
+ {children}
+
+ ),
}))
const mockUseDetailContext = useDetailContext as jest.MockedFunction
@@ -81,4 +90,14 @@ describe('LocalitySpeciesTab MW score prerequisites', () => {
const cellResult = cellRenderer?.({ row })
expect(cellResult).toBeTruthy()
})
+
+ it('uses occurrence terminology in grouped heading and modal button', () => {
+ render()
+
+ const grouped = document.querySelector('[data-testid="grouped"]')
+ const editingModal = document.querySelector('[data-testid="editing-modal"]')
+
+ expect(grouped?.getAttribute('data-title')).toBe(occurrenceLabels.informationSectionTitle)
+ expect(editingModal?.getAttribute('data-button-text')).toBe(occurrenceLabels.addNewButton)
+ })
})
diff --git a/frontend/tests/locality-species-ui-label-inventory.md b/frontend/tests/locality-species-ui-label-inventory.md
new file mode 100644
index 00000000..604986ad
--- /dev/null
+++ b/frontend/tests/locality-species-ui-label-inventory.md
@@ -0,0 +1,55 @@
+# Locality-Species UI Label Inventory (Task T1)
+
+This checklist inventories user-visible "Locality-Species" terminology in frontend UI surfaces and maps each item to the intended replacement term.
+
+## Legend
+- **Singular target**: `Occurrence`
+- **Plural/collection target**: `Occurrences`
+
+## Inventory Checklist
+
+- [x] `frontend/src/components/NavBar.tsx` (line 23)
+ - Current UI label: `Locality-Species` (navigation item)
+ - Context: navigation entry linking to `/crosssearch`
+ - Target label: **Occurrences** (plural, module/list entry)
+
+- [x] `frontend/src/components/CrossSearch/CrossSearchTable.tsx` (line 820)
+ - Current UI label: `Locality-Species-Cross-Search` (table/view title)
+ - Context: cross-search page caption/title
+ - Target label: **Occurrence Cross-Search** (collection search surface)
+
+- [x] `frontend/src/components/Species/SpeciesDetails.tsx` (line 108)
+ - Current UI label: `Locality Species` (tab title)
+ - Context: species details tab that lists linked rows
+ - Target label: **Occurrences** (plural tab content)
+
+- [x] `frontend/src/components/Species/Tabs/LocalitySpeciesTab.tsx` (line 200)
+ - Current UI label: `Add new Locality Species` (action button text)
+ - Context: action creates one new row
+ - Target label: **Add new Occurrence** (singular action target)
+
+- [x] `frontend/src/components/Species/Tabs/LocalitySpeciesTab.tsx` (line 215)
+ - Current UI label: `Locality-Species Information` (group section title)
+ - Context: grouped information panel describing row collection
+ - Target label: **Occurrence Information** (domain section heading)
+
+- [x] `frontend/src/components/FrontPage.tsx` (line 75)
+ - Current UI label: `Locality-species` (statistics card label)
+ - Context: count of all rows in collection
+ - Target label: **Occurrences** (plural count label)
+
+## Related Non-UI Mentions (Do Not Rename in this task)
+
+These are not user-facing captions and are intentionally excluded from UI wording replacement:
+
+- Type names and identifiers (e.g. `LocalitySpecies`, `LocalitySpeciesDetailsType`) in shared/frontend TypeScript.
+- File names and test suite names that reference implementation details.
+- Export filename slug in `frontend/src/components/CrossSearch/CrossSearchExportMenuItem.tsx` (`locality-species-...csv`) unless product decides file naming should also change.
+- Internal comparisons/guards in `frontend/src/components/TableView/TableView.tsx` using the legacy title token.
+
+## Validation Notes
+
+- Database table and backend contracts remain unchanged (`now_ls` is not renamed in this feature).
+- Search terms used: `Locality-Species`, `Locality Species`, `Locality-species`, `Locality-Species-Cross-Search`, `locality-species`.
+- Coverage scope: `frontend/src/**` and `frontend/tests/**`.
+- Result: all discovered user-visible captions are listed above and tagged with singular/plural replacement intent.