Files
marco/tests/acceptance/search-loading-test.js
Râu Cao 62407f5fa4 Sync search form query value
* Clear input when clearing search from anywhere
* Pre-fill input when opening search URL with query params
2026-06-30 19:12:38 +02:00

246 lines
6.7 KiB
JavaScript

import { module, test } from 'qunit';
import { visit, click, fillIn, currentURL, settled } from '@ember/test-helpers';
import { setupApplicationTest } from 'marco/tests/helpers';
import Service from '@ember/service';
import { Promise } from 'rsvp';
let photonResolve;
let osmResolve;
class MockPhotonService extends Service {
cancelAll() {}
async search(query) {
if (query === 'slow') {
// Return a promise that we can manually resolve in the test
// to avoid race conditions with native setTimeout
return new Promise((resolve) => {
photonResolve = () => {
resolve([
{
title: 'Test Place',
lat: 1,
lon: 1,
osmId: '123',
osmType: 'node',
},
]);
};
});
}
return [
{
title: 'Test Place',
lat: 1,
lon: 1,
osmId: '123',
osmType: 'node',
},
];
}
}
class MockOsmService extends Service {
cancelAll() {}
async getCategoryPois(bounds, category) {
if (category === 'slow_category') {
return new Promise((resolve) => {
osmResolve = () => {
resolve([]);
};
});
}
return [];
}
async getNearbyPois() {
return [];
}
}
module('Acceptance | search loading', function (hooks) {
setupApplicationTest(hooks);
hooks.beforeEach(function () {
photonResolve = null;
osmResolve = null;
this.owner.register('service:photon', MockPhotonService);
this.owner.register('service:osm', MockOsmService);
});
test('search shows loading indicator but nearby search does not', async function (assert) {
const mapUi = this.owner.lookup('service:map-ui');
// 1. Text Search
// Start a search and check for loading state immediately
const searchPromise = visit('/search?q=slow');
// We can't easily check the DOM mid-transition in acceptance tests without complicated helpers,
// so we check the service state which drives the UI.
// Wait a tiny bit for the route to start processing
await new Promise((r) => setTimeout(r, 10));
assert.deepEqual(
mapUi.loadingState,
{ type: 'text', value: 'slow' },
'Loading state is set for text search'
);
// Resolve the manual promise so the task can finish deterministically
photonResolve();
await searchPromise;
await settled(); // Wait for ember-concurrency tasks to fully settle
assert.strictEqual(
mapUi.loadingState,
null,
'Loading state is cleared after text search'
);
// 2. Category Search
const catPromise = visit('/search?category=slow_category&lat=1&lon=1');
await new Promise((r) => setTimeout(r, 10));
assert.deepEqual(
mapUi.loadingState,
{ type: 'category', value: 'slow_category' },
'Loading state is set for category search'
);
// Resolve the manual promise
osmResolve();
await catPromise;
await settled();
assert.strictEqual(
mapUi.loadingState,
null,
'Loading state is cleared after category search'
);
// 3. Nearby Search
await visit('/search?lat=1&lon=1');
assert.strictEqual(
mapUi.loadingState,
null,
'Loading state is NOT set for nearby search'
);
});
test('clearing search stops loading indicator', async function (assert) {
const mapUi = this.owner.lookup('service:map-ui');
// 1. Start from index
await visit('/');
// 2. Type "slow" to trigger autocomplete (which is async)
await fillIn('.search-input', 'slow');
// 3. Submit search to trigger route loading
click('.search-submit-btn'); // Intentionally no await to not block on transition
// Wait for loading state to activate
await new Promise((r) => setTimeout(r, 100));
assert.deepEqual(
mapUi.loadingState,
{ type: 'text', value: 'slow' },
'Loading state is set'
);
// 4. Click the clear button (should be visible since input has value)
await click('.search-clear-btn');
// Wait for the click and transition to settle
// Verify loading state is cleared immediately
assert.strictEqual(
mapUi.loadingState,
null,
'Loading state is cleared immediately after clicking clear'
);
// Clean up the dangling promise
if (photonResolve) {
photonResolve();
}
// Verify we are back on index (or at least query is gone)
assert.strictEqual(currentURL(), '/', 'Navigated to index');
});
test('quick search pills visibility during category search transition', async function (assert) {
const mapUi = this.owner.lookup('service:map-ui');
mapUi.currentZoom = 15;
// Seed localStorage with a high zoom level to ensure quick search buttons show
const highZoomState = {
center: [13.4, 52.5],
zoom: 18,
};
window.localStorage.setItem(
'marco:map-view',
JSON.stringify(highZoomState)
);
try {
// Make sure quick search buttons setting is enabled
const settings = this.owner.lookup('service:settings');
settings.showQuickSearchButtons = true;
// 1. Visit slowly loading category search
const catPromise = visit('/search?category=slow_category&lat=1&lon=1');
await new Promise((r) => setTimeout(r, 10));
// Verify loading state is set and pills are visible (i.e. header-center does NOT have .searching)
assert.ok(mapUi.loadingState, 'Search is loading');
assert
.dom('.header-center')
.doesNotHaveClass(
'searching',
'Pills remain visible while search is loading'
);
// Resolve the promise with empty results
osmResolve();
await catPromise;
await settled();
// Verify search completed and since results are empty, pills are still visible
assert.strictEqual(mapUi.searchResults.length, 0, 'No results found');
assert
.dom('.header-center')
.doesNotHaveClass(
'searching',
'Pills remain visible after search completes with no results'
);
// 2. Now simulate a fast category search that returns results
const osmService = this.owner.lookup('service:osm');
osmService.getCategoryPois = async () => [
{
title: 'Latte Art Cafe',
lat: 1,
lon: 1,
osmId: '101',
osmType: 'N',
},
];
await visit('/search?category=coffee&lat=1&lon=1');
// Verify search completed with results, so pills are hidden
assert.ok(mapUi.searchResults.length > 0, 'Results found');
assert
.dom('.header-center')
.hasClass(
'searching',
'Pills are hidden after search completes with results'
);
} finally {
window.localStorage.removeItem('marco:map-view');
}
});
});