Compare commits
5 Commits
7285ace882
...
0f8d7046ac
| Author | SHA1 | Date | |
|---|---|---|---|
|
0f8d7046ac
|
|||
|
8ca7481a79
|
|||
|
cd25c55bd7
|
|||
|
32c4f7da57
|
|||
|
71939a30c3
|
@@ -23,7 +23,7 @@ export default class Blurhash extends Component {
|
|||||||
imageData.data.set(pixels);
|
imageData.data.set(pixels);
|
||||||
ctx.putImageData(imageData, 0, 0);
|
ctx.putImageData(imageData, 0, 0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to decode blurhash:', e);
|
console.warn('Failed to decode blurhash:', e.message || e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1034,7 +1034,7 @@ export default class MapComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleMapMove = async () => {
|
handleMapMove = async () => {
|
||||||
if (!this.mapInstance) return;
|
if (!this.mapInstance || this.isDestroying || this.isDestroyed) return;
|
||||||
|
|
||||||
const view = this.mapInstance.getView();
|
const view = this.mapInstance.getView();
|
||||||
const center = toLonLat(view.getCenter());
|
const center = toLonLat(view.getCenter());
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export default class PlacePhotosCarousel extends Component {
|
|||||||
disabled={{this.cannotScrollLeft}}
|
disabled={{this.cannotScrollLeft}}
|
||||||
aria-label="Previous photo"
|
aria-label="Previous photo"
|
||||||
>
|
>
|
||||||
<Icon @name="chevron-left" />
|
<Icon @name="chevron-left" @color="currentColor" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -146,7 +146,7 @@ export default class PlacePhotosCarousel extends Component {
|
|||||||
disabled={{this.cannotScrollRight}}
|
disabled={{this.cannotScrollRight}}
|
||||||
aria-label="Next photo"
|
aria-label="Next photo"
|
||||||
>
|
>
|
||||||
<Icon @name="chevron-right" />
|
<Icon @name="chevron-right" @color="currentColor" />
|
||||||
</button>
|
</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -950,11 +950,11 @@ abbr[title] {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.place-photos-carousel-wrapper:hover .carousel-nav-btn {
|
.place-photos-carousel-wrapper:hover .carousel-nav-btn:not(.disabled) {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.carousel-nav-btn:hover {
|
.carousel-nav-btn:not(.disabled):hover {
|
||||||
background: rgb(0 0 0 / 80%);
|
background: rgb(0 0 0 / 80%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -973,9 +973,8 @@ abbr[title] {
|
|||||||
|
|
||||||
@media (width <= 768px) {
|
@media (width <= 768px) {
|
||||||
.place-photos-carousel-track {
|
.place-photos-carousel-track {
|
||||||
scroll-snap-type: none; /* No snapping on mobile */
|
scroll-snap-type: none;
|
||||||
gap: 0.25rem;
|
gap: 2px;
|
||||||
padding-bottom: 0.5rem; /* Space for the scrollbar if visible, but we hid it */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.carousel-slide {
|
.carousel-slide {
|
||||||
@@ -988,7 +987,6 @@ abbr[title] {
|
|||||||
|
|
||||||
.place-header-photo.landscape,
|
.place-header-photo.landscape,
|
||||||
.place-header-photo.portrait {
|
.place-header-photo.portrait {
|
||||||
/* On mobile, all images use cover inside their precise ratio container */
|
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
setupRenderingTest as upstreamSetupRenderingTest,
|
setupRenderingTest as upstreamSetupRenderingTest,
|
||||||
setupTest as upstreamSetupTest,
|
setupTest as upstreamSetupTest,
|
||||||
} from 'ember-qunit';
|
} from 'ember-qunit';
|
||||||
|
import { setupNostrMocks } from './mock-nostr';
|
||||||
|
|
||||||
// This file exists to provide wrappers around ember-qunit's
|
// This file exists to provide wrappers around ember-qunit's
|
||||||
// test setup functions. This way, you can easily extend the setup that is
|
// test setup functions. This way, you can easily extend the setup that is
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
|
|
||||||
function setupApplicationTest(hooks, options) {
|
function setupApplicationTest(hooks, options) {
|
||||||
upstreamSetupApplicationTest(hooks, options);
|
upstreamSetupApplicationTest(hooks, options);
|
||||||
|
setupNostrMocks(hooks);
|
||||||
|
|
||||||
// Additional setup for application tests can be done here.
|
// Additional setup for application tests can be done here.
|
||||||
//
|
//
|
||||||
@@ -29,12 +31,14 @@ function setupApplicationTest(hooks, options) {
|
|||||||
|
|
||||||
function setupRenderingTest(hooks, options) {
|
function setupRenderingTest(hooks, options) {
|
||||||
upstreamSetupRenderingTest(hooks, options);
|
upstreamSetupRenderingTest(hooks, options);
|
||||||
|
setupNostrMocks(hooks);
|
||||||
|
|
||||||
// Additional setup for rendering tests can be done here.
|
// Additional setup for rendering tests can be done here.
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupTest(hooks, options) {
|
function setupTest(hooks, options) {
|
||||||
upstreamSetupTest(hooks, options);
|
upstreamSetupTest(hooks, options);
|
||||||
|
setupNostrMocks(hooks);
|
||||||
|
|
||||||
// Additional setup for unit tests can be done here.
|
// Additional setup for unit tests can be done here.
|
||||||
}
|
}
|
||||||
|
|||||||
96
tests/helpers/mock-nostr.js
Normal file
96
tests/helpers/mock-nostr.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import Service from '@ember/service';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { Promise } from 'rsvp';
|
||||||
|
|
||||||
|
export class MockNostrAuthService extends Service {
|
||||||
|
@tracked pubkey = null;
|
||||||
|
@tracked signerType = null;
|
||||||
|
@tracked connectStatus = null;
|
||||||
|
@tracked connectUri = null;
|
||||||
|
|
||||||
|
get isConnected() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isMobile() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get signer() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectWithExtension() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectWithApp() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MockNostrDataService extends Service {
|
||||||
|
@tracked profile = null;
|
||||||
|
@tracked mailboxes = null;
|
||||||
|
@tracked blossomServers = [];
|
||||||
|
@tracked placePhotos = [];
|
||||||
|
|
||||||
|
store = {
|
||||||
|
add: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
get activeReadRelays() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeWriteRelays() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get defaultReadRelays() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get defaultWriteRelays() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get userDisplayName() {
|
||||||
|
return 'Mock User';
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPlacesInBounds() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPhotosForPlace() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPlacePhotos() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MockNostrRelayService extends Service {
|
||||||
|
pool = {
|
||||||
|
publish: () => Promise.resolve([{ ok: true }]),
|
||||||
|
subscribe: () => {},
|
||||||
|
unsubscribe: () => {},
|
||||||
|
close: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
async publish() {
|
||||||
|
return [{ ok: true }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupNostrMocks(hooks) {
|
||||||
|
hooks.beforeEach(function () {
|
||||||
|
this.owner.register('service:nostrAuth', MockNostrAuthService);
|
||||||
|
this.owner.register('service:nostrData', MockNostrDataService);
|
||||||
|
this.owner.register('service:nostrRelay', MockNostrRelayService);
|
||||||
|
});
|
||||||
|
}
|
||||||
109
tests/integration/components/place-photos-carousel-test.gjs
Normal file
109
tests/integration/components/place-photos-carousel-test.gjs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { setupRenderingTest } from 'marco/tests/helpers';
|
||||||
|
import { render, click } from '@ember/test-helpers';
|
||||||
|
import PlacePhotosCarousel from 'marco/components/place-photos-carousel';
|
||||||
|
|
||||||
|
module('Integration | Component | place-photos-carousel', function (hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
test('it renders gracefully with no photos', async function (assert) {
|
||||||
|
this.photos = [];
|
||||||
|
|
||||||
|
await render(
|
||||||
|
<template><PlacePhotosCarousel @photos={{this.photos}} /></template>
|
||||||
|
);
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom('.place-photos-carousel-wrapper')
|
||||||
|
.doesNotExist('it does not render the wrapper when there are no photos');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders a single photo without navigation chevrons', async function (assert) {
|
||||||
|
this.photos = [
|
||||||
|
{
|
||||||
|
url: 'photo1.jpg',
|
||||||
|
thumbUrl: 'thumb1.jpg',
|
||||||
|
blurhash: 'LKO2?U%2Tw=w]~RBVZRi};RPxuwH',
|
||||||
|
ratio: 1.5,
|
||||||
|
isLandscape: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await render(
|
||||||
|
<template>
|
||||||
|
<div class="test-container">
|
||||||
|
<PlacePhotosCarousel @photos={{this.photos}} />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
);
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom('.place-photos-carousel-wrapper')
|
||||||
|
.exists('it renders the wrapper');
|
||||||
|
assert.dom('.carousel-slide').exists({ count: 1 }, 'it renders one slide');
|
||||||
|
assert
|
||||||
|
.dom('img.place-header-photo')
|
||||||
|
.hasAttribute('src', 'photo1.jpg', 'it renders the photo');
|
||||||
|
|
||||||
|
// There should be no chevrons when there's only 1 photo
|
||||||
|
assert
|
||||||
|
.dom('.carousel-nav-btn')
|
||||||
|
.doesNotExist('it does not render chevrons for a single photo');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders multiple photos and shows chevrons', async function (assert) {
|
||||||
|
this.photos = [
|
||||||
|
{
|
||||||
|
url: 'photo1.jpg',
|
||||||
|
thumbUrl: 'thumb1.jpg',
|
||||||
|
blurhash: 'LKO2?U%2Tw=w]~RBVZRi};RPxuwH',
|
||||||
|
ratio: 1.5,
|
||||||
|
isLandscape: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'photo2.jpg',
|
||||||
|
thumbUrl: 'thumb2.jpg',
|
||||||
|
blurhash: 'LKO2?U%2Tw=w]~RBVZRi};RPxuwH',
|
||||||
|
ratio: 1.0,
|
||||||
|
isLandscape: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'photo3.jpg',
|
||||||
|
thumbUrl: 'thumb3.jpg',
|
||||||
|
blurhash: 'LKO2?U%2Tw=w]~RBVZRi};RPxuwH',
|
||||||
|
ratio: 0.8,
|
||||||
|
isLandscape: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await render(
|
||||||
|
<template>
|
||||||
|
<div class="test-container">
|
||||||
|
<PlacePhotosCarousel @photos={{this.photos}} />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
assert.dom('.carousel-slide').exists({ count: 3 }, 'it renders all slides');
|
||||||
|
assert
|
||||||
|
.dom('.carousel-nav-btn')
|
||||||
|
.exists({ count: 2 }, 'it renders both chevrons');
|
||||||
|
|
||||||
|
// Initially, it shouldn't be able to scroll left
|
||||||
|
assert
|
||||||
|
.dom('.carousel-nav-btn.prev')
|
||||||
|
.hasClass('disabled', 'the prev button is disabled initially');
|
||||||
|
assert
|
||||||
|
.dom('.carousel-nav-btn.next')
|
||||||
|
.doesNotHaveClass('disabled', 'the next button is enabled initially');
|
||||||
|
|
||||||
|
// We can't perfectly test native scroll behavior easily in JSDOM/QUnit without mocking the DOM elements' scroll properties,
|
||||||
|
// but we can test that clicking the next button triggers the scrolling method.
|
||||||
|
// However, since we mock scrollLeft in the component logic implicitly via template action, let's at least ensure clicking doesn't throw.
|
||||||
|
await click('.carousel-nav-btn.next');
|
||||||
|
|
||||||
|
assert.ok(true, 'clicking next button does not throw');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user