Optionally add tag to place photo
This commit is contained in:
@@ -8,6 +8,10 @@ import { task } from 'ember-concurrency';
|
||||
import Geohash from 'latlon-geohash';
|
||||
import PlacePhotoUploadItem from './place-photo-upload-item';
|
||||
import Icon from '#components/icon';
|
||||
import { getSuggestedPhotoTags } from '../utils/photo-tag-suggestions';
|
||||
import capitalize from '../helpers/capitalize';
|
||||
import includes from '../helpers/includes';
|
||||
import { fn } from '@ember/helper';
|
||||
import { or, not } from 'ember-truth-helpers';
|
||||
|
||||
export default class PlacePhotoUpload extends Component {
|
||||
@@ -22,6 +26,7 @@ export default class PlacePhotoUpload extends Component {
|
||||
@tracked error = '';
|
||||
@tracked isPublishing = false;
|
||||
@tracked isDragging = false;
|
||||
@tracked selectedTags = [];
|
||||
|
||||
get place() {
|
||||
return this.args.place || {};
|
||||
@@ -37,6 +42,10 @@ export default class PlacePhotoUpload extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
get suggestedTags() {
|
||||
return getSuggestedPhotoTags(this.place);
|
||||
}
|
||||
|
||||
@action
|
||||
handleFileSelect(event) {
|
||||
this.addFile(event.target.files[0]);
|
||||
@@ -93,11 +102,22 @@ export default class PlacePhotoUpload extends Component {
|
||||
}
|
||||
this.file = null;
|
||||
this.uploadedPhoto = null;
|
||||
this.selectedTags = [];
|
||||
if (this.args.onUploadStateChange) {
|
||||
this.args.onUploadStateChange(false);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
toggleTag(tag) {
|
||||
if (this.selectedTags.includes(tag)) {
|
||||
this.selectedTags = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedTags = [tag];
|
||||
}
|
||||
|
||||
deletePhotoTask = task(async (photoData) => {
|
||||
try {
|
||||
if (photoData.hash) {
|
||||
@@ -139,6 +159,10 @@ export default class PlacePhotoUpload extends Component {
|
||||
|
||||
const tags = [['i', `osm:${osmType}:${osmId}`]];
|
||||
|
||||
for (const tag of this.selectedTags) {
|
||||
tags.push(['t', tag]);
|
||||
}
|
||||
|
||||
if (lat && lon) {
|
||||
tags.push(['g', Geohash.encode(lat, lon, 4)]);
|
||||
tags.push(['g', Geohash.encode(lat, lon, 6)]);
|
||||
@@ -227,6 +251,26 @@ export default class PlacePhotoUpload extends Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{#if this.suggestedTags.length}}
|
||||
<div class="photo-tag-suggestions">
|
||||
<p class="photo-tag-suggestions-title">
|
||||
Choose a tag/category (optional):
|
||||
</p>
|
||||
<div class="photo-tag-suggestions-list">
|
||||
{{#each this.suggestedTags as |tag|}}
|
||||
<button
|
||||
type="button"
|
||||
class="photo-tag-chip
|
||||
{{if (includes this.selectedTags tag) 'is-selected'}}"
|
||||
{{on "click" (fn this.toggleTag tag)}}
|
||||
>
|
||||
{{capitalize tag}}
|
||||
</button>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-publish"
|
||||
|
||||
6
app/helpers/capitalize.js
Normal file
6
app/helpers/capitalize.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { helper } from '@ember/component/helper';
|
||||
import { capitalize as format } from '../utils/format-text';
|
||||
|
||||
export default helper(function capitalize([text]) {
|
||||
return format(text);
|
||||
});
|
||||
6
app/helpers/includes.js
Normal file
6
app/helpers/includes.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { helper } from '@ember/component/helper';
|
||||
|
||||
export default helper(function includes([collection, value]) {
|
||||
if (!Array.isArray(collection)) return false;
|
||||
return collection.includes(value);
|
||||
});
|
||||
@@ -1843,6 +1843,42 @@ button.create-place {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.photo-tag-suggestions {
|
||||
margin: 1rem 0 1.5rem;
|
||||
}
|
||||
|
||||
.photo-tag-suggestions-title {
|
||||
color: #898989;
|
||||
margin: 0 0 0.75rem;
|
||||
font-weight: normal;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.photo-tag-suggestions-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.photo-tag-chip {
|
||||
background: #f8f9fa;
|
||||
color: #333;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
padding: 6px 12px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s;
|
||||
}
|
||||
|
||||
.photo-tag-chip:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.photo-tag-chip.is-selected {
|
||||
background: rgb(255 204 51 / 30%);
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
@@ -7,3 +7,8 @@ export function humanizeOsmTag(text) {
|
||||
w.replace(/^\w/, (c) => c.toUpperCase())
|
||||
);
|
||||
}
|
||||
|
||||
export function capitalize(text) {
|
||||
if (typeof text !== 'string' || !text) return '';
|
||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,11 @@ export function parsePlacePhotos(events) {
|
||||
const allPhotos = [];
|
||||
|
||||
for (const event of sortedEvents) {
|
||||
const eventTagValues = event.tags
|
||||
.filter((t) => t[0] === 't')
|
||||
.map((t) => t[1])
|
||||
.filter(Boolean);
|
||||
|
||||
// Find all imeta tags
|
||||
const imetas = event.tags.filter((t) => t[0] === 'imeta');
|
||||
for (const imeta of imetas) {
|
||||
@@ -70,6 +75,7 @@ export function parsePlacePhotos(events) {
|
||||
isLandscape,
|
||||
aspectRatio,
|
||||
placeIdentifier,
|
||||
tags: eventTagValues,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
32
app/utils/photo-tag-suggestions.js
Normal file
32
app/utils/photo-tag-suggestions.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { POI_CATEGORIES } from './poi-categories';
|
||||
import { getMatchingPoiCategoryIds } from './poi-category-matcher';
|
||||
|
||||
export const CATEGORY_TAGS = {
|
||||
restaurants: ['food', 'menu', 'vibe', 'front'],
|
||||
coffee: ['food', 'menu', 'vibe', 'front'],
|
||||
groceries: ['front', 'food'],
|
||||
'things-to-do': ['architecture', 'amenities', 'vibe', 'front'],
|
||||
accommodation: ['rooms', 'amenities', 'food', 'vibe', 'front'],
|
||||
};
|
||||
|
||||
export function getSuggestedPhotoTags(place) {
|
||||
const osmTags = place?.osmTags || place?.tags || {};
|
||||
const categoryIds = getMatchingPoiCategoryIds(osmTags, POI_CATEGORIES);
|
||||
|
||||
const suggested = [];
|
||||
for (const categoryId of categoryIds) {
|
||||
const tags = CATEGORY_TAGS[categoryId];
|
||||
if (!Array.isArray(tags)) continue;
|
||||
for (const tag of tags) {
|
||||
if (!suggested.includes(tag)) {
|
||||
suggested.push(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (suggested.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return suggested;
|
||||
}
|
||||
95
app/utils/poi-category-matcher.js
Normal file
95
app/utils/poi-category-matcher.js
Normal file
@@ -0,0 +1,95 @@
|
||||
export function getMatchingPoiCategories(osmTags, categories) {
|
||||
if (!Array.isArray(categories) || !osmTags) return [];
|
||||
|
||||
return categories.filter((category) => {
|
||||
if (!Array.isArray(category.filter)) return false;
|
||||
return category.filter.some((filterStr) =>
|
||||
matchesFilter(osmTags, filterStr)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function getMatchingPoiCategoryIds(osmTags, categories) {
|
||||
return getMatchingPoiCategories(osmTags, categories).map((c) => c.id);
|
||||
}
|
||||
|
||||
function matchesFilter(osmTags, filterStr) {
|
||||
const clauses = parseOverpassClauses(filterStr);
|
||||
if (clauses.length === 0) return false;
|
||||
return clauses.every((clause) => matchesClause(osmTags, clause));
|
||||
}
|
||||
|
||||
function parseOverpassClauses(filterStr) {
|
||||
if (!filterStr) return [];
|
||||
const matches = filterStr.match(/\[[^\]]+\]/g);
|
||||
if (!matches) return [];
|
||||
|
||||
return matches
|
||||
.map((raw) => parseClause(raw.slice(1, -1).trim()))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function parseClause(content) {
|
||||
const presenceMatch = content.match(/^"([^"]+)"$/);
|
||||
if (presenceMatch) {
|
||||
return { type: 'presence', key: presenceMatch[1] };
|
||||
}
|
||||
|
||||
const equalsMatch = content.match(/^"([^"]+)"\s*=\s*"([^"]*)"$/);
|
||||
if (equalsMatch) {
|
||||
return { type: 'equals', key: equalsMatch[1], value: equalsMatch[2] };
|
||||
}
|
||||
|
||||
const regexMatch = content.match(/^"([^"]+)"\s*~\s*"([^"]*)"$/);
|
||||
if (regexMatch) {
|
||||
return {
|
||||
type: 'regex',
|
||||
key: regexMatch[1],
|
||||
pattern: regexMatch[2],
|
||||
regex: new RegExp(regexMatch[2]),
|
||||
};
|
||||
}
|
||||
|
||||
const notRegexMatch = content.match(/^"([^"]+)"\s*!~\s*"([^"]*)"$/);
|
||||
if (notRegexMatch) {
|
||||
return {
|
||||
type: 'not-regex',
|
||||
key: notRegexMatch[1],
|
||||
pattern: notRegexMatch[2],
|
||||
regex: new RegExp(notRegexMatch[2]),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function matchesClause(osmTags, clause) {
|
||||
const tagValues = getTagValues(osmTags, clause.key);
|
||||
|
||||
switch (clause.type) {
|
||||
case 'presence':
|
||||
return tagValues.length > 0;
|
||||
case 'equals':
|
||||
return tagValues.some((value) => value === clause.value);
|
||||
case 'regex':
|
||||
return tagValues.some((value) => clause.regex.test(value));
|
||||
case 'not-regex':
|
||||
return (
|
||||
tagValues.length === 0 ||
|
||||
!tagValues.some((value) => clause.regex.test(value))
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getTagValues(osmTags, key) {
|
||||
if (!osmTags || !key) return [];
|
||||
const rawValue = osmTags[key];
|
||||
if (rawValue === undefined || rawValue === null) return [];
|
||||
|
||||
return String(rawValue)
|
||||
.split(';')
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
100
tests/integration/components/place-photo-upload-test.gjs
Normal file
100
tests/integration/components/place-photo-upload-test.gjs
Normal file
@@ -0,0 +1,100 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'marco/tests/helpers';
|
||||
import { render, click, triggerEvent } from '@ember/test-helpers';
|
||||
import Service from '@ember/service';
|
||||
import PlacePhotoUpload from 'marco/components/place-photo-upload';
|
||||
|
||||
module('Integration | Component | place-photo-upload', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
class MockNostrAuthService extends Service {
|
||||
get isConnected() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get signer() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.owner.register('service:nostrAuth', MockNostrAuthService);
|
||||
});
|
||||
|
||||
async function selectFile(element, file) {
|
||||
const input = element.querySelector('#photo-upload-input');
|
||||
Object.defineProperty(input, 'files', {
|
||||
value: [file],
|
||||
configurable: true,
|
||||
});
|
||||
await triggerEvent(input, 'change');
|
||||
}
|
||||
|
||||
test('it shows tag suggestions when they exist after upload selection', async function (assert) {
|
||||
this.place = {
|
||||
title: 'Cafe Alpha',
|
||||
osmId: '123',
|
||||
osmType: 'node',
|
||||
osmTags: { amenity: 'cafe' },
|
||||
};
|
||||
|
||||
await render(
|
||||
<template><PlacePhotoUpload @place={{this.place}} /></template>
|
||||
);
|
||||
|
||||
assert.dom('.photo-tag-suggestions').doesNotExist();
|
||||
|
||||
const file = new File(['test'], 'photo.jpg', { type: 'image/jpeg' });
|
||||
await selectFile(this.element, file);
|
||||
|
||||
assert.dom('.photo-tag-suggestions').exists();
|
||||
assert.dom('.photo-tag-chip').exists();
|
||||
assert.dom('.photo-tag-chip').includesText('Food');
|
||||
});
|
||||
|
||||
test('it only allows one selected tag at a time', async function (assert) {
|
||||
this.place = {
|
||||
title: 'Cafe Alpha',
|
||||
osmId: '123',
|
||||
osmType: 'node',
|
||||
osmTags: { amenity: 'cafe' },
|
||||
};
|
||||
|
||||
await render(
|
||||
<template><PlacePhotoUpload @place={{this.place}} /></template>
|
||||
);
|
||||
|
||||
const file = new File(['test'], 'photo.jpg', { type: 'image/jpeg' });
|
||||
await selectFile(this.element, file);
|
||||
|
||||
const chips = this.element.querySelectorAll('.photo-tag-chip');
|
||||
assert.ok(chips.length > 1, 'multiple tag chips are rendered');
|
||||
|
||||
await click(chips[0]);
|
||||
assert.dom('.photo-tag-chip.is-selected').exists({ count: 1 });
|
||||
assert.dom(chips[0]).hasClass('is-selected');
|
||||
|
||||
await click(chips[1]);
|
||||
assert.dom('.photo-tag-chip.is-selected').exists({ count: 1 });
|
||||
assert.dom(chips[1]).hasClass('is-selected');
|
||||
assert.dom(chips[0]).doesNotHaveClass('is-selected');
|
||||
});
|
||||
|
||||
test('it hides tag suggestions when no tags are suggested', async function (assert) {
|
||||
this.place = {
|
||||
title: 'Office Beta',
|
||||
osmId: '456',
|
||||
osmType: 'node',
|
||||
osmTags: { office: 'lawyer' },
|
||||
};
|
||||
|
||||
await render(
|
||||
<template><PlacePhotoUpload @place={{this.place}} /></template>
|
||||
);
|
||||
|
||||
const file = new File(['test'], 'photo.jpg', { type: 'image/jpeg' });
|
||||
await selectFile(this.element, file);
|
||||
|
||||
assert.dom('.photo-tag-suggestions').doesNotExist();
|
||||
});
|
||||
});
|
||||
144
tests/unit/utils/nostr-test.js
Normal file
144
tests/unit/utils/nostr-test.js
Normal file
@@ -0,0 +1,144 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { normalizeRelayUrl, parsePlacePhotos } from 'marco/utils/nostr';
|
||||
|
||||
module('Unit | Utility | nostr', function () {
|
||||
test('normalizeRelayUrl normalizes protocol, case, and slashes', function (assert) {
|
||||
assert.strictEqual(normalizeRelayUrl(null), '');
|
||||
assert.strictEqual(normalizeRelayUrl(''), '');
|
||||
assert.strictEqual(normalizeRelayUrl(' '), '');
|
||||
|
||||
assert.strictEqual(
|
||||
normalizeRelayUrl('Relay.example.com'),
|
||||
'wss://relay.example.com'
|
||||
);
|
||||
assert.strictEqual(
|
||||
normalizeRelayUrl('ws://Relay.example.com/'),
|
||||
'ws://relay.example.com'
|
||||
);
|
||||
assert.strictEqual(
|
||||
normalizeRelayUrl('wss://relay.example.com///'),
|
||||
'wss://relay.example.com'
|
||||
);
|
||||
});
|
||||
|
||||
test('parsePlacePhotos includes event t tags on photo objects', function (assert) {
|
||||
const events = [
|
||||
{
|
||||
id: 'event-1',
|
||||
pubkey: 'pubkey-1',
|
||||
created_at: 123,
|
||||
tags: [
|
||||
['i', 'osm:node:123'],
|
||||
['t', 'food'],
|
||||
['t', 'vibe'],
|
||||
['imeta', 'url https://example.com/photo.jpg', 'dim 800x600'],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const photos = parsePlacePhotos(events);
|
||||
|
||||
assert.strictEqual(photos.length, 1);
|
||||
assert.deepEqual(photos[0].tags, ['food', 'vibe']);
|
||||
});
|
||||
|
||||
test('parsePlacePhotos sorts by created_at', function (assert) {
|
||||
const events = [
|
||||
{
|
||||
id: 'event-2',
|
||||
pubkey: 'pubkey-2',
|
||||
created_at: 200,
|
||||
tags: [
|
||||
['i', 'osm:node:456'],
|
||||
['imeta', 'url https://example.com/late.jpg', 'dim 600x900'],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'event-1',
|
||||
pubkey: 'pubkey-1',
|
||||
created_at: 100,
|
||||
tags: [
|
||||
['i', 'osm:node:123'],
|
||||
['imeta', 'url https://example.com/early.jpg', 'dim 600x900'],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const photos = parsePlacePhotos(events);
|
||||
|
||||
assert.strictEqual(photos.length, 2);
|
||||
assert.strictEqual(photos[0].url, 'https://example.com/early.jpg');
|
||||
assert.strictEqual(photos[1].url, 'https://example.com/late.jpg');
|
||||
});
|
||||
|
||||
test('parsePlacePhotos promotes first landscape photo to index 0', function (assert) {
|
||||
const events = [
|
||||
{
|
||||
id: 'event-1',
|
||||
pubkey: 'pubkey-1',
|
||||
created_at: 100,
|
||||
tags: [
|
||||
['imeta', 'url https://example.com/portrait.jpg', 'dim 600x900'],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'event-2',
|
||||
pubkey: 'pubkey-2',
|
||||
created_at: 200,
|
||||
tags: [
|
||||
['imeta', 'url https://example.com/landscape.jpg', 'dim 1200x600'],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const photos = parsePlacePhotos(events);
|
||||
|
||||
assert.strictEqual(photos.length, 2);
|
||||
assert.strictEqual(photos[0].url, 'https://example.com/landscape.jpg');
|
||||
assert.strictEqual(photos[1].url, 'https://example.com/portrait.jpg');
|
||||
});
|
||||
|
||||
test('parsePlacePhotos skips imeta entries without urls', function (assert) {
|
||||
const events = [
|
||||
{
|
||||
id: 'event-1',
|
||||
pubkey: 'pubkey-1',
|
||||
created_at: 100,
|
||||
tags: [['imeta', 'dim 800x600']],
|
||||
},
|
||||
];
|
||||
|
||||
const photos = parsePlacePhotos(events);
|
||||
|
||||
assert.deepEqual(photos, []);
|
||||
});
|
||||
|
||||
test('parsePlacePhotos returns one photo per event imeta tag', function (assert) {
|
||||
const events = [
|
||||
{
|
||||
id: 'event-1',
|
||||
pubkey: 'pubkey-1',
|
||||
created_at: 100,
|
||||
tags: [
|
||||
['i', 'osm:node:123'],
|
||||
['imeta', 'url https://example.com/photo-1.jpg', 'dim 800x600'],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'event-2',
|
||||
pubkey: 'pubkey-2',
|
||||
created_at: 200,
|
||||
tags: [
|
||||
['i', 'osm:node:456'],
|
||||
['imeta', 'url https://example.com/photo-2.jpg', 'dim 600x800'],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const photos = parsePlacePhotos(events);
|
||||
|
||||
assert.strictEqual(photos.length, 2);
|
||||
assert.strictEqual(photos[0].placeIdentifier, 'osm:node:123');
|
||||
assert.strictEqual(photos[1].placeIdentifier, 'osm:node:456');
|
||||
});
|
||||
});
|
||||
30
tests/unit/utils/photo-tag-suggestions-test.js
Normal file
30
tests/unit/utils/photo-tag-suggestions-test.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { POI_CATEGORIES } from 'marco/utils/poi-categories';
|
||||
import { getMatchingPoiCategoryIds } from 'marco/utils/poi-category-matcher';
|
||||
import {
|
||||
getSuggestedPhotoTags,
|
||||
CATEGORY_TAGS,
|
||||
} from 'marco/utils/photo-tag-suggestions';
|
||||
|
||||
module('Unit | Utility | photo-tag-suggestions', function () {
|
||||
test('returns tags for all matching categories with de-duplication', function (assert) {
|
||||
const place = { osmTags: { amenity: 'cafe' } };
|
||||
const categoryIds = getMatchingPoiCategoryIds(
|
||||
place.osmTags,
|
||||
POI_CATEGORIES
|
||||
);
|
||||
|
||||
assert.ok(categoryIds.includes('restaurants'));
|
||||
assert.ok(categoryIds.includes('coffee'));
|
||||
|
||||
const result = getSuggestedPhotoTags(place);
|
||||
assert.deepEqual(result, CATEGORY_TAGS.restaurants);
|
||||
});
|
||||
|
||||
test('returns no tags when no category matches', function (assert) {
|
||||
const place = { osmTags: { office: 'lawyer' } };
|
||||
const result = getSuggestedPhotoTags(place);
|
||||
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
});
|
||||
38
tests/unit/utils/poi-category-matcher-test.js
Normal file
38
tests/unit/utils/poi-category-matcher-test.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { POI_CATEGORIES } from 'marco/utils/poi-categories';
|
||||
import {
|
||||
getMatchingPoiCategories,
|
||||
getMatchingPoiCategoryIds,
|
||||
} from 'marco/utils/poi-category-matcher';
|
||||
|
||||
module('Unit | Utility | poi-category-matcher', function () {
|
||||
test('matches multiple categories from OSM tags', function (assert) {
|
||||
const tags = { amenity: 'cafe' };
|
||||
const categoryIds = getMatchingPoiCategoryIds(tags, POI_CATEGORIES);
|
||||
|
||||
assert.ok(categoryIds.includes('restaurants'));
|
||||
assert.ok(categoryIds.includes('coffee'));
|
||||
});
|
||||
|
||||
test('supports semicolon-separated values', function (assert) {
|
||||
const tags = { amenity: 'cafe;bar' };
|
||||
const categoryIds = getMatchingPoiCategoryIds(tags, POI_CATEGORIES);
|
||||
|
||||
assert.ok(categoryIds.includes('coffee'));
|
||||
});
|
||||
|
||||
test('negative regex clause fails if any value matches', function (assert) {
|
||||
const tags = { amenity: 'cafe', cuisine: 'coffee;irish' };
|
||||
const categoryIds = getMatchingPoiCategoryIds(tags, POI_CATEGORIES);
|
||||
|
||||
assert.notOk(categoryIds.includes('restaurants'));
|
||||
});
|
||||
|
||||
test('presence clause matches when tag exists', function (assert) {
|
||||
const tags = { historic: 'castle' };
|
||||
const categories = getMatchingPoiCategories(tags, POI_CATEGORIES);
|
||||
const categoryIds = categories.map((category) => category.id);
|
||||
|
||||
assert.ok(categoryIds.includes('things-to-do'));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user