Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
60 changes: 42 additions & 18 deletions src/components/form.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,15 @@ customElements.define('cycloops-form', CycloopsForm, { extends: 'form' })
vi.mock('../db', () => ({
db: {
notes: {
add: vi.fn(),
add: vi.fn().mockResolvedValue(1),
update: vi.fn().mockResolvedValue(1),
},
},
}))

// Mock geolocation
const mockGeolocation = {
getCurrentPosition: vi.fn().mockImplementation((success) =>
Promise.resolve(
success({
coords: {
latitude: 50,
longitude: 50,
},
})
)
),
getCurrentPosition: vi.fn(),
}
vi.stubGlobal('navigator', { geolocation: mockGeolocation })

Expand All @@ -37,22 +29,54 @@ describe('CycloopsForm component', () => {
form.innerHTML = '<textarea name="message"></textarea>'
document.body.appendChild(form) // This should trigger connectedCallback
vi.clearAllMocks()
;(db.notes.add as ReturnType<typeof vi.fn>).mockResolvedValue(1)
;(db.notes.update as ReturnType<typeof vi.fn>).mockResolvedValue(1)
})

it('should add a note on submit', async () => {
it('should optimistically add a note immediately with placeholder coords', async () => {
mockGeolocation.getCurrentPosition.mockImplementation(() => {
// never resolves during this test
})

const textarea = form.querySelector('textarea') as HTMLTextAreaElement
textarea.value = 'Test message'

// The submit handler is async, so we need to wait for it to complete
await form.submitHandler(new Event('submit'))

expect(db.notes.add).toHaveBeenCalledOnce()
expect(db.notes.add).toHaveBeenCalledWith(
expect.objectContaining({
text: 'Test message',
lat: 50,
lon: 50,
})
expect.objectContaining({ text: 'Test message', lat: 0, lon: 0 })
)
})

it('should update coords after geolocation resolves', async () => {
mockGeolocation.getCurrentPosition.mockImplementation((success: PositionCallback) =>
success({ coords: { latitude: 50, longitude: 50 } } as GeolocationPosition)
)

const textarea = form.querySelector('textarea') as HTMLTextAreaElement
textarea.value = 'Test message'

await form.submitHandler(new Event('submit'))

expect(db.notes.update).toHaveBeenCalledWith(1, { lat: 50, lon: 50 })
})

it('should not add a note when message is whitespace only', async () => {
const textarea = form.querySelector('textarea') as HTMLTextAreaElement
textarea.value = ' '

await form.submitHandler(new Event('submit'))

expect(db.notes.add).not.toHaveBeenCalled()
})

it('should not add a note when message is blank', async () => {
const textarea = form.querySelector('textarea') as HTMLTextAreaElement
textarea.value = ''

await form.submitHandler(new Event('submit'))

expect(db.notes.add).not.toHaveBeenCalled()
})
})
40 changes: 19 additions & 21 deletions src/components/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,28 @@ export class CycloopsForm extends HTMLFormElement {
e.preventDefault();

const data = new FormData(this);
const text = data.get("message") as string;
const time = Date.now();
const text = (data.get("message") as string).trim();

const [lat, lon] = await new Promise<[number, number]>(
(resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(position) => {
console.log(position.coords);
resolve([position.coords.latitude, position.coords.longitude]);
},
() => {
resolve([0, 0]);
}
);
}
);
if (!text) return;

const time = Date.now();

await db.notes.add({
time,
text,
lat,
lon,
});
// Optimistically add the note immediately so it appears in the list
const id = await db.notes.add({ time, text, lat: 0, lon: 0 });

this.reset();

// Update with real coordinates once geolocation resolves
navigator.geolocation.getCurrentPosition(
async (position) => {
await db.notes.update(id, {
lat: position.coords.latitude,
lon: position.coords.longitude,
});
},
() => {
// Keep lat/lon as 0,0 on failure — note is already saved
}
);
}
}
7 changes: 7 additions & 0 deletions src/components/map.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ describe('map computed signals', () => {
expect(noteLocations.value.features[1].properties.id).toBe(2)
})

it('noteLocations should exclude notes with placeholder 0,0 coordinates', () => {
const placeholder: Note = { id: 3, time: Date.now(), text: 'Pending', lat: 0, lon: 0 }
notes.value = [note1, placeholder]
expect(noteLocations.value.features.length).toBe(1)
expect(noteLocations.value.features[0].properties.id).toBe(1)
})

it('visibleLocations should filter notes based on visibility', () => {
notes.value = [note1, note2]
visible.value = new Set([1])
Expand Down
4 changes: 3 additions & 1 deletion src/components/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ type Locations = GeoJSON.FeatureCollection<

export const noteLocations = computed<Locations>(() => ({
type: "FeatureCollection",
features: notes.value.map((note) => ({
features: notes.value
.filter((note) => note.lat !== 0 || note.lon !== 0)
.map((note) => ({
type: "Feature",
properties: {
id: note.id,
Expand Down