263 lines
7.2 KiB
JavaScript
263 lines
7.2 KiB
JavaScript
import { module, test } from 'qunit';
|
|
import { groupPhotoContributions } from 'marco/utils/contributions';
|
|
|
|
function makePhotoEvent({
|
|
id,
|
|
created_at,
|
|
placeIdentifier = 'osm:node:123',
|
|
url = 'https://example.com/photo.jpg',
|
|
tags = [],
|
|
}) {
|
|
return {
|
|
id,
|
|
pubkey: 'pubkey-1',
|
|
kind: 360,
|
|
created_at,
|
|
tags: [
|
|
['i', placeIdentifier],
|
|
['imeta', `url ${url}`, 'dim 800x600'],
|
|
...tags.map((t) => ['t', t]),
|
|
],
|
|
};
|
|
}
|
|
|
|
function makeDeletionEvent({ id, targetId }) {
|
|
return {
|
|
id,
|
|
pubkey: 'pubkey-1',
|
|
kind: 5,
|
|
created_at: 999,
|
|
tags: [['e', targetId]],
|
|
};
|
|
}
|
|
|
|
module('Unit | Utility | contributions', function () {
|
|
test('groupPhotoContributions returns empty for no events', function (assert) {
|
|
assert.deepEqual(groupPhotoContributions([]), []);
|
|
assert.deepEqual(groupPhotoContributions(null), []);
|
|
});
|
|
|
|
test('groupPhotoContributions creates one entry per event for the same place when spread out', function (assert) {
|
|
// Two events 10 hours apart for the same place -> two entries
|
|
const events = [
|
|
makePhotoEvent({
|
|
id: 'e1',
|
|
created_at: 1000,
|
|
url: 'https://x.com/1.jpg',
|
|
}),
|
|
makePhotoEvent({
|
|
id: 'e2',
|
|
created_at: 1000 + 10 * 60 * 60,
|
|
url: 'https://x.com/2.jpg',
|
|
}),
|
|
];
|
|
|
|
const entries = groupPhotoContributions(events);
|
|
assert.strictEqual(entries.length, 2);
|
|
// Newest first
|
|
assert.strictEqual(entries[0].photos[0].url, 'https://x.com/2.jpg');
|
|
assert.strictEqual(entries[1].photos[0].url, 'https://x.com/1.jpg');
|
|
});
|
|
|
|
test('groupPhotoContributions groups events for the same place within the threshold', function (assert) {
|
|
// Two events 30 minutes apart -> one entry with two photos
|
|
const events = [
|
|
makePhotoEvent({
|
|
id: 'e1',
|
|
created_at: 1000,
|
|
url: 'https://x.com/1.jpg',
|
|
}),
|
|
makePhotoEvent({
|
|
id: 'e2',
|
|
created_at: 1000 + 30 * 60,
|
|
url: 'https://x.com/2.jpg',
|
|
}),
|
|
];
|
|
|
|
const entries = groupPhotoContributions(events);
|
|
assert.strictEqual(entries.length, 1);
|
|
assert.strictEqual(entries[0].photos.length, 2);
|
|
assert.strictEqual(entries[0].eventCount, 2);
|
|
// createdAt = newest event's created_at
|
|
assert.strictEqual(entries[0].createdAt, 1000 + 30 * 60);
|
|
});
|
|
|
|
test('groupPhotoContributions orders photos within a group oldest-first', function (assert) {
|
|
// Three events within the threshold for the same place
|
|
const events = [
|
|
makePhotoEvent({
|
|
id: 'e3',
|
|
created_at: 3000,
|
|
url: 'https://x.com/3.jpg',
|
|
}),
|
|
makePhotoEvent({
|
|
id: 'e1',
|
|
created_at: 1000,
|
|
url: 'https://x.com/1.jpg',
|
|
}),
|
|
makePhotoEvent({
|
|
id: 'e2',
|
|
created_at: 2000,
|
|
url: 'https://x.com/2.jpg',
|
|
}),
|
|
];
|
|
|
|
const entries = groupPhotoContributions(events);
|
|
assert.strictEqual(entries.length, 1);
|
|
assert.strictEqual(
|
|
entries[0].photos[0].url,
|
|
'https://x.com/1.jpg',
|
|
'First photo is the oldest'
|
|
);
|
|
assert.strictEqual(
|
|
entries[0].photos[1].url,
|
|
'https://x.com/2.jpg',
|
|
'Second photo is the middle'
|
|
);
|
|
assert.strictEqual(
|
|
entries[0].photos[2].url,
|
|
'https://x.com/3.jpg',
|
|
'Third photo is the newest'
|
|
);
|
|
});
|
|
|
|
test('groupPhotoContributions groups by OSM entity identifier', function (assert) {
|
|
const events = [
|
|
makePhotoEvent({
|
|
id: 'e1',
|
|
created_at: 1000,
|
|
placeIdentifier: 'osm:node:111',
|
|
url: 'https://x.com/a.jpg',
|
|
}),
|
|
makePhotoEvent({
|
|
id: 'e2',
|
|
created_at: 1000,
|
|
placeIdentifier: 'osm:node:222',
|
|
url: 'https://x.com/b.jpg',
|
|
}),
|
|
];
|
|
|
|
const entries = groupPhotoContributions(events);
|
|
assert.strictEqual(entries.length, 2);
|
|
});
|
|
|
|
test('groupPhotoContributions sorts entries newest-first', function (assert) {
|
|
const events = [
|
|
makePhotoEvent({
|
|
id: 'old',
|
|
created_at: 100,
|
|
placeIdentifier: 'osm:node:111',
|
|
}),
|
|
makePhotoEvent({
|
|
id: 'new',
|
|
created_at: 200,
|
|
placeIdentifier: 'osm:node:222',
|
|
}),
|
|
makePhotoEvent({
|
|
id: 'mid',
|
|
created_at: 150,
|
|
placeIdentifier: 'osm:node:333',
|
|
}),
|
|
];
|
|
|
|
const entries = groupPhotoContributions(events);
|
|
assert.strictEqual(entries[0].placeIdentifier, 'osm:node:222');
|
|
assert.strictEqual(entries[1].placeIdentifier, 'osm:node:333');
|
|
assert.strictEqual(entries[2].placeIdentifier, 'osm:node:111');
|
|
});
|
|
|
|
test('groupPhotoContributions respects custom threshold hours', function (assert) {
|
|
// Events 2 hours apart. With threshold 1, they are separate; with 3, they group.
|
|
const events = [
|
|
makePhotoEvent({
|
|
id: 'e1',
|
|
created_at: 1000,
|
|
url: 'https://x.com/1.jpg',
|
|
}),
|
|
makePhotoEvent({
|
|
id: 'e2',
|
|
created_at: 1000 + 2 * 60 * 60,
|
|
url: 'https://x.com/2.jpg',
|
|
}),
|
|
];
|
|
|
|
assert.strictEqual(groupPhotoContributions(events, 1).length, 2);
|
|
assert.strictEqual(groupPhotoContributions(events, 3).length, 1);
|
|
});
|
|
|
|
test('groupPhotoContributions parses place identifier into osmType and osmId', function (assert) {
|
|
const events = [
|
|
makePhotoEvent({
|
|
id: 'e1',
|
|
created_at: 1000,
|
|
placeIdentifier: 'osm:way:98765',
|
|
}),
|
|
];
|
|
|
|
const entries = groupPhotoContributions(events);
|
|
assert.strictEqual(entries[0].osmType, 'way');
|
|
assert.strictEqual(entries[0].osmId, '98765');
|
|
});
|
|
|
|
test('groupPhotoContributions attaches t tags to photos', function (assert) {
|
|
const events = [
|
|
makePhotoEvent({
|
|
id: 'e1',
|
|
created_at: 1000,
|
|
tags: ['food', 'vibe'],
|
|
}),
|
|
];
|
|
|
|
const entries = groupPhotoContributions(events);
|
|
assert.deepEqual(entries[0].photos[0].tags, ['food', 'vibe']);
|
|
});
|
|
|
|
test('groupPhotoContributions excludes events deleted by kind 5', function (assert) {
|
|
const events = [
|
|
makePhotoEvent({
|
|
id: 'e1',
|
|
created_at: 1000,
|
|
url: 'https://x.com/1.jpg',
|
|
}),
|
|
makePhotoEvent({
|
|
id: 'e2',
|
|
created_at: 2000,
|
|
url: 'https://x.com/2.jpg',
|
|
}),
|
|
makeDeletionEvent({ id: 'd1', targetId: 'e1' }),
|
|
];
|
|
|
|
const entries = groupPhotoContributions(events);
|
|
assert.strictEqual(entries.length, 1);
|
|
assert.strictEqual(entries[0].photos[0].url, 'https://x.com/2.jpg');
|
|
});
|
|
|
|
test('groupPhotoContributions skips events without an i tag', function (assert) {
|
|
const events = [
|
|
{
|
|
id: 'no-i',
|
|
pubkey: 'pubkey-1',
|
|
kind: 360,
|
|
created_at: 1000,
|
|
tags: [['imeta', 'url https://x.com/1.jpg', 'dim 800x600']],
|
|
},
|
|
];
|
|
|
|
const entries = groupPhotoContributions(events);
|
|
assert.strictEqual(entries.length, 0);
|
|
});
|
|
|
|
test('groupPhotoContributions skips events without a usable imeta', function (assert) {
|
|
const events = [makePhotoEvent({ id: 'e1', created_at: 1000, url: null })];
|
|
|
|
// The helper always sets a URL, so override manually
|
|
events[0].tags = [
|
|
['i', 'osm:node:123'],
|
|
['imeta', 'dim 800x600'],
|
|
];
|
|
|
|
const entries = groupPhotoContributions(events);
|
|
assert.strictEqual(entries.length, 0);
|
|
});
|
|
});
|