mirror of
https://github.com/immich-app/immich.git
synced 2026-02-04 08:49:01 +03:00
fix(web): enable asset viewer navigation across memory boundaries (#25741)
This commit is contained in:
2
e2e/src/generators/memory.ts
Normal file
2
e2e/src/generators/memory.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { generateMemoriesFromTimeline, generateMemory } from './memory/model-objects';
|
||||
export type { MemoryConfig, MemoryYearConfig } from './memory/model-objects';
|
||||
84
e2e/src/generators/memory/model-objects.ts
Normal file
84
e2e/src/generators/memory/model-objects.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { MemoryType, type MemoryResponseDto, type OnThisDayDto } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { toAssetResponseDto } from 'src/generators/timeline/rest-response';
|
||||
import type { MockTimelineAsset } from 'src/generators/timeline/timeline-config';
|
||||
import { SeededRandom, selectRandomMultiple } from 'src/generators/timeline/utils';
|
||||
|
||||
export type MemoryConfig = {
|
||||
id?: string;
|
||||
ownerId: string;
|
||||
year: number;
|
||||
memoryAt: string;
|
||||
isSaved?: boolean;
|
||||
};
|
||||
|
||||
export type MemoryYearConfig = {
|
||||
year: number;
|
||||
assetCount: number;
|
||||
};
|
||||
|
||||
export function generateMemory(config: MemoryConfig, assets: MockTimelineAsset[]): MemoryResponseDto {
|
||||
const now = new Date().toISOString();
|
||||
const memoryId = config.id ?? faker.string.uuid();
|
||||
|
||||
return {
|
||||
id: memoryId,
|
||||
assets: assets.map((asset) => toAssetResponseDto(asset)),
|
||||
data: { year: config.year } as OnThisDayDto,
|
||||
memoryAt: config.memoryAt,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
isSaved: config.isSaved ?? false,
|
||||
ownerId: config.ownerId,
|
||||
type: MemoryType.OnThisDay,
|
||||
};
|
||||
}
|
||||
|
||||
export function generateMemoriesFromTimeline(
|
||||
timelineAssets: MockTimelineAsset[],
|
||||
ownerId: string,
|
||||
memoryConfigs: MemoryYearConfig[],
|
||||
seed: number = 42,
|
||||
): MemoryResponseDto[] {
|
||||
const rng = new SeededRandom(seed);
|
||||
const memories: MemoryResponseDto[] = [];
|
||||
const usedAssetIds = new Set<string>();
|
||||
|
||||
for (const config of memoryConfigs) {
|
||||
const yearAssets = timelineAssets.filter((asset) => {
|
||||
const assetYear = DateTime.fromISO(asset.fileCreatedAt).year;
|
||||
return assetYear === config.year && !usedAssetIds.has(asset.id);
|
||||
});
|
||||
|
||||
if (yearAssets.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const countToSelect = Math.min(config.assetCount, yearAssets.length);
|
||||
const selectedAssets = selectRandomMultiple(yearAssets, countToSelect, rng);
|
||||
|
||||
for (const asset of selectedAssets) {
|
||||
usedAssetIds.add(asset.id);
|
||||
}
|
||||
|
||||
selectedAssets.sort(
|
||||
(a, b) => DateTime.fromISO(b.fileCreatedAt).diff(DateTime.fromISO(a.fileCreatedAt)).milliseconds,
|
||||
);
|
||||
|
||||
const memoryAt = DateTime.now().set({ year: config.year }).toISO()!;
|
||||
|
||||
memories.push(
|
||||
generateMemory(
|
||||
{
|
||||
ownerId,
|
||||
year: config.year,
|
||||
memoryAt,
|
||||
},
|
||||
selectedAssets,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return memories;
|
||||
}
|
||||
65
e2e/src/mock-network/memory-network.ts
Normal file
65
e2e/src/mock-network/memory-network.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { MemoryResponseDto } from '@immich/sdk';
|
||||
import { BrowserContext } from '@playwright/test';
|
||||
|
||||
export type MemoryChanges = {
|
||||
memoryDeletions: string[];
|
||||
assetRemovals: Map<string, string[]>;
|
||||
};
|
||||
|
||||
export const setupMemoryMockApiRoutes = async (
|
||||
context: BrowserContext,
|
||||
memories: MemoryResponseDto[],
|
||||
changes: MemoryChanges,
|
||||
) => {
|
||||
await context.route('**/api/memories*', async (route, request) => {
|
||||
const url = new URL(request.url());
|
||||
const pathname = url.pathname;
|
||||
|
||||
if (pathname === '/api/memories' && request.method() === 'GET') {
|
||||
const activeMemories = memories
|
||||
.filter((memory) => !changes.memoryDeletions.includes(memory.id))
|
||||
.map((memory) => {
|
||||
const removedAssets = changes.assetRemovals.get(memory.id) ?? [];
|
||||
return {
|
||||
...memory,
|
||||
assets: memory.assets.filter((asset) => !removedAssets.includes(asset.id)),
|
||||
};
|
||||
})
|
||||
.filter((memory) => memory.assets.length > 0);
|
||||
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
json: activeMemories,
|
||||
});
|
||||
}
|
||||
|
||||
const memoryMatch = pathname.match(/\/api\/memories\/([^/]+)$/);
|
||||
if (memoryMatch && request.method() === 'GET') {
|
||||
const memoryId = memoryMatch[1];
|
||||
const memory = memories.find((m) => m.id === memoryId);
|
||||
|
||||
if (!memory || changes.memoryDeletions.includes(memoryId)) {
|
||||
return route.fulfill({ status: 404 });
|
||||
}
|
||||
|
||||
const removedAssets = changes.assetRemovals.get(memoryId) ?? [];
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
json: {
|
||||
...memory,
|
||||
assets: memory.assets.filter((asset) => !removedAssets.includes(asset.id)),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (/\/api\/memories\/([^/]+)$/.test(pathname) && request.method() === 'DELETE') {
|
||||
const memoryId = pathname.split('/').pop()!;
|
||||
changes.memoryDeletions.push(memoryId);
|
||||
return route.fulfill({ status: 204 });
|
||||
}
|
||||
|
||||
await route.fallback();
|
||||
});
|
||||
};
|
||||
289
e2e/src/web/specs/memory/memory-viewer.ui-spec.ts
Normal file
289
e2e/src/web/specs/memory/memory-viewer.ui-spec.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { MemoryResponseDto } from '@immich/sdk';
|
||||
import { test } from '@playwright/test';
|
||||
import { generateMemoriesFromTimeline } from 'src/generators/memory';
|
||||
import {
|
||||
Changes,
|
||||
createDefaultTimelineConfig,
|
||||
generateTimelineData,
|
||||
TimelineAssetConfig,
|
||||
TimelineData,
|
||||
} from 'src/generators/timeline';
|
||||
import { setupBaseMockApiRoutes } from 'src/mock-network/base-network';
|
||||
import { MemoryChanges, setupMemoryMockApiRoutes } from 'src/mock-network/memory-network';
|
||||
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network';
|
||||
import { memoryAssetViewerUtils, memoryGalleryUtils, memoryViewerUtils } from 'src/web/specs/memory/utils';
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
test.describe('Memory Viewer - Gallery Asset Viewer Navigation', () => {
|
||||
let adminUserId: string;
|
||||
let timelineRestData: TimelineData;
|
||||
let memories: MemoryResponseDto[];
|
||||
const assets: TimelineAssetConfig[] = [];
|
||||
const testContext = new TimelineTestContext();
|
||||
const changes: Changes = {
|
||||
albumAdditions: [],
|
||||
assetDeletions: [],
|
||||
assetArchivals: [],
|
||||
assetFavorites: [],
|
||||
};
|
||||
const memoryChanges: MemoryChanges = {
|
||||
memoryDeletions: [],
|
||||
assetRemovals: new Map(),
|
||||
};
|
||||
|
||||
test.beforeAll(async () => {
|
||||
adminUserId = faker.string.uuid();
|
||||
testContext.adminId = adminUserId;
|
||||
|
||||
timelineRestData = generateTimelineData({
|
||||
...createDefaultTimelineConfig(),
|
||||
ownerId: adminUserId,
|
||||
});
|
||||
|
||||
for (const timeBucket of timelineRestData.buckets.values()) {
|
||||
assets.push(...timeBucket);
|
||||
}
|
||||
|
||||
memories = generateMemoriesFromTimeline(
|
||||
assets,
|
||||
adminUserId,
|
||||
[
|
||||
{ year: 2024, assetCount: 3 },
|
||||
{ year: 2023, assetCount: 2 },
|
||||
{ year: 2022, assetCount: 4 },
|
||||
],
|
||||
42,
|
||||
);
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ context }) => {
|
||||
await setupBaseMockApiRoutes(context, adminUserId);
|
||||
await setupTimelineMockApiRoutes(context, timelineRestData, changes, testContext);
|
||||
await setupMemoryMockApiRoutes(context, memories, memoryChanges);
|
||||
});
|
||||
|
||||
test.afterEach(() => {
|
||||
testContext.slowBucket = false;
|
||||
changes.albumAdditions = [];
|
||||
changes.assetDeletions = [];
|
||||
changes.assetArchivals = [];
|
||||
changes.assetFavorites = [];
|
||||
memoryChanges.memoryDeletions = [];
|
||||
memoryChanges.assetRemovals.clear();
|
||||
});
|
||||
|
||||
test.describe('Asset viewer navigation from gallery', () => {
|
||||
test('shows both prev/next buttons for middle asset within a memory', async ({ page }) => {
|
||||
const firstMemory = memories[0];
|
||||
const middleAsset = firstMemory.assets[1];
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, middleAsset.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, middleAsset.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, middleAsset);
|
||||
|
||||
await memoryAssetViewerUtils.expectPreviousButtonVisible(page);
|
||||
await memoryAssetViewerUtils.expectNextButtonVisible(page);
|
||||
});
|
||||
|
||||
test('shows next button when at last asset of first memory (next memory exists)', async ({ page }) => {
|
||||
const firstMemory = memories[0];
|
||||
const lastAssetOfFirstMemory = firstMemory.assets.at(-1)!;
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, lastAssetOfFirstMemory.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, lastAssetOfFirstMemory.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, lastAssetOfFirstMemory);
|
||||
|
||||
await memoryAssetViewerUtils.expectNextButtonVisible(page);
|
||||
await memoryAssetViewerUtils.expectPreviousButtonVisible(page);
|
||||
});
|
||||
|
||||
test('shows prev button when at first asset of last memory (prev memory exists)', async ({ page }) => {
|
||||
const lastMemory = memories.at(-1)!;
|
||||
const firstAssetOfLastMemory = lastMemory.assets[0];
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, firstAssetOfLastMemory.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, firstAssetOfLastMemory.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, firstAssetOfLastMemory);
|
||||
|
||||
await memoryAssetViewerUtils.expectPreviousButtonVisible(page);
|
||||
await memoryAssetViewerUtils.expectNextButtonVisible(page);
|
||||
});
|
||||
|
||||
test('can navigate from last asset of memory to first asset of next memory', async ({ page }) => {
|
||||
const firstMemory = memories[0];
|
||||
const secondMemory = memories[1];
|
||||
const lastAssetOfFirst = firstMemory.assets.at(-1)!;
|
||||
const firstAssetOfSecond = secondMemory.assets[0];
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, lastAssetOfFirst.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, lastAssetOfFirst.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, lastAssetOfFirst);
|
||||
|
||||
await memoryAssetViewerUtils.clickNextButton(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, firstAssetOfSecond);
|
||||
|
||||
await memoryAssetViewerUtils.expectCurrentAssetId(page, firstAssetOfSecond.id);
|
||||
});
|
||||
|
||||
test('can navigate from first asset of memory to last asset of previous memory', async ({ page }) => {
|
||||
const firstMemory = memories[0];
|
||||
const secondMemory = memories[1];
|
||||
const lastAssetOfFirst = firstMemory.assets.at(-1)!;
|
||||
const firstAssetOfSecond = secondMemory.assets[0];
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, firstAssetOfSecond.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, firstAssetOfSecond.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, firstAssetOfSecond);
|
||||
|
||||
await memoryAssetViewerUtils.clickPreviousButton(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, lastAssetOfFirst);
|
||||
});
|
||||
|
||||
test('hides prev button at very first asset (first memory, first asset, no prev memory)', async ({ page }) => {
|
||||
const firstMemory = memories[0];
|
||||
const veryFirstAsset = firstMemory.assets[0];
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, veryFirstAsset.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, veryFirstAsset.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, veryFirstAsset);
|
||||
|
||||
await memoryAssetViewerUtils.expectPreviousButtonNotVisible(page);
|
||||
await memoryAssetViewerUtils.expectNextButtonVisible(page);
|
||||
});
|
||||
|
||||
test('hides next button at very last asset (last memory, last asset, no next memory)', async ({ page }) => {
|
||||
const lastMemory = memories.at(-1)!;
|
||||
const veryLastAsset = lastMemory.assets.at(-1)!;
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, veryLastAsset.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, veryLastAsset.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, veryLastAsset);
|
||||
|
||||
await memoryAssetViewerUtils.expectNextButtonNotVisible(page);
|
||||
await memoryAssetViewerUtils.expectPreviousButtonVisible(page);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Keyboard navigation', () => {
|
||||
test('ArrowLeft navigates to previous asset across memory boundary', async ({ page }) => {
|
||||
const firstMemory = memories[0];
|
||||
const secondMemory = memories[1];
|
||||
const lastAssetOfFirst = firstMemory.assets.at(-1)!;
|
||||
const firstAssetOfSecond = secondMemory.assets[0];
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, firstAssetOfSecond.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, firstAssetOfSecond.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, firstAssetOfSecond);
|
||||
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, lastAssetOfFirst);
|
||||
});
|
||||
|
||||
test('ArrowRight navigates to next asset across memory boundary', async ({ page }) => {
|
||||
const firstMemory = memories[0];
|
||||
const secondMemory = memories[1];
|
||||
const lastAssetOfFirst = firstMemory.assets.at(-1)!;
|
||||
const firstAssetOfSecond = secondMemory.assets[0];
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, lastAssetOfFirst.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, lastAssetOfFirst.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, lastAssetOfFirst);
|
||||
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, firstAssetOfSecond);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Memory Viewer - Single Asset Memory Edge Cases', () => {
|
||||
let adminUserId: string;
|
||||
let timelineRestData: TimelineData;
|
||||
let memories: MemoryResponseDto[];
|
||||
const assets: TimelineAssetConfig[] = [];
|
||||
const testContext = new TimelineTestContext();
|
||||
const changes: Changes = {
|
||||
albumAdditions: [],
|
||||
assetDeletions: [],
|
||||
assetArchivals: [],
|
||||
assetFavorites: [],
|
||||
};
|
||||
const memoryChanges: MemoryChanges = {
|
||||
memoryDeletions: [],
|
||||
assetRemovals: new Map(),
|
||||
};
|
||||
|
||||
test.beforeAll(async () => {
|
||||
adminUserId = faker.string.uuid();
|
||||
testContext.adminId = adminUserId;
|
||||
|
||||
timelineRestData = generateTimelineData({
|
||||
...createDefaultTimelineConfig(),
|
||||
ownerId: adminUserId,
|
||||
});
|
||||
|
||||
for (const timeBucket of timelineRestData.buckets.values()) {
|
||||
assets.push(...timeBucket);
|
||||
}
|
||||
|
||||
memories = generateMemoriesFromTimeline(
|
||||
assets,
|
||||
adminUserId,
|
||||
[
|
||||
{ year: 2024, assetCount: 2 },
|
||||
{ year: 2023, assetCount: 1 },
|
||||
{ year: 2022, assetCount: 2 },
|
||||
],
|
||||
123,
|
||||
);
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ context }) => {
|
||||
await setupBaseMockApiRoutes(context, adminUserId);
|
||||
await setupTimelineMockApiRoutes(context, timelineRestData, changes, testContext);
|
||||
await setupMemoryMockApiRoutes(context, memories, memoryChanges);
|
||||
});
|
||||
|
||||
test.afterEach(() => {
|
||||
testContext.slowBucket = false;
|
||||
changes.albumAdditions = [];
|
||||
changes.assetDeletions = [];
|
||||
changes.assetArchivals = [];
|
||||
changes.assetFavorites = [];
|
||||
memoryChanges.memoryDeletions = [];
|
||||
memoryChanges.assetRemovals.clear();
|
||||
});
|
||||
|
||||
test('single asset memory shows both prev/next when surrounded by other memories', async ({ page }) => {
|
||||
const singleAssetMemory = memories[1];
|
||||
const singleAsset = singleAssetMemory.assets[0];
|
||||
|
||||
await memoryViewerUtils.openMemoryPageWithAsset(page, singleAsset.id);
|
||||
await memoryGalleryUtils.clickThumbnail(page, singleAsset.id);
|
||||
|
||||
await memoryAssetViewerUtils.waitForViewerOpen(page);
|
||||
await memoryAssetViewerUtils.waitForAssetLoad(page, singleAsset);
|
||||
|
||||
await memoryAssetViewerUtils.expectPreviousButtonVisible(page);
|
||||
await memoryAssetViewerUtils.expectNextButtonVisible(page);
|
||||
});
|
||||
});
|
||||
123
e2e/src/web/specs/memory/utils.ts
Normal file
123
e2e/src/web/specs/memory/utils.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { AssetResponseDto } from '@immich/sdk';
|
||||
import { expect, Page } from '@playwright/test';
|
||||
|
||||
function getAssetIdFromUrl(url: URL): string | null {
|
||||
const pathMatch = url.pathname.match(/\/memory\/photos\/([^/]+)/);
|
||||
if (pathMatch) {
|
||||
return pathMatch[1];
|
||||
}
|
||||
return url.searchParams.get('id');
|
||||
}
|
||||
|
||||
export const memoryViewerUtils = {
|
||||
locator(page: Page) {
|
||||
return page.locator('#memory-viewer');
|
||||
},
|
||||
|
||||
async waitForMemoryLoad(page: Page) {
|
||||
await expect(this.locator(page)).toBeVisible();
|
||||
await expect(page.locator('#memory-viewer img').first()).toBeVisible();
|
||||
},
|
||||
|
||||
async openMemoryPage(page: Page) {
|
||||
await page.goto('/memory');
|
||||
await this.waitForMemoryLoad(page);
|
||||
},
|
||||
|
||||
async openMemoryPageWithAsset(page: Page, assetId: string) {
|
||||
await page.goto(`/memory?id=${assetId}`);
|
||||
await this.waitForMemoryLoad(page);
|
||||
},
|
||||
};
|
||||
|
||||
export const memoryGalleryUtils = {
|
||||
locator(page: Page) {
|
||||
return page.locator('#gallery-memory');
|
||||
},
|
||||
|
||||
thumbnailWithAssetId(page: Page, assetId: string) {
|
||||
return page.locator(`#gallery-memory [data-thumbnail-focus-container][data-asset="${assetId}"]`);
|
||||
},
|
||||
|
||||
async scrollToGallery(page: Page) {
|
||||
const showGalleryButton = page.getByLabel('Show gallery');
|
||||
if (await showGalleryButton.isVisible()) {
|
||||
await showGalleryButton.click();
|
||||
}
|
||||
await expect(this.locator(page)).toBeInViewport();
|
||||
},
|
||||
|
||||
async clickThumbnail(page: Page, assetId: string) {
|
||||
await this.scrollToGallery(page);
|
||||
await this.thumbnailWithAssetId(page, assetId).click();
|
||||
},
|
||||
|
||||
async getAllThumbnails(page: Page) {
|
||||
await this.scrollToGallery(page);
|
||||
return page.locator('#gallery-memory [data-thumbnail-focus-container]');
|
||||
},
|
||||
};
|
||||
|
||||
export const memoryAssetViewerUtils = {
|
||||
locator(page: Page) {
|
||||
return page.locator('#immich-asset-viewer');
|
||||
},
|
||||
|
||||
async waitForViewerOpen(page: Page) {
|
||||
await expect(this.locator(page)).toBeVisible();
|
||||
},
|
||||
|
||||
async waitForAssetLoad(page: Page, asset: AssetResponseDto) {
|
||||
const viewer = this.locator(page);
|
||||
const imgLocator = viewer.locator(`img[draggable="false"][src*="/api/assets/${asset.id}/thumbnail?size=preview"]`);
|
||||
const videoLocator = viewer.locator(`video[poster*="/api/assets/${asset.id}/thumbnail?size=preview"]`);
|
||||
|
||||
await imgLocator.or(videoLocator).waitFor({ timeout: 10_000 });
|
||||
},
|
||||
|
||||
nextButton(page: Page) {
|
||||
return page.getByLabel('View next asset');
|
||||
},
|
||||
|
||||
previousButton(page: Page) {
|
||||
return page.getByLabel('View previous asset');
|
||||
},
|
||||
|
||||
async expectNextButtonVisible(page: Page) {
|
||||
await expect(this.nextButton(page)).toBeVisible();
|
||||
},
|
||||
|
||||
async expectNextButtonNotVisible(page: Page) {
|
||||
await expect(this.nextButton(page)).toHaveCount(0);
|
||||
},
|
||||
|
||||
async expectPreviousButtonVisible(page: Page) {
|
||||
await expect(this.previousButton(page)).toBeVisible();
|
||||
},
|
||||
|
||||
async expectPreviousButtonNotVisible(page: Page) {
|
||||
await expect(this.previousButton(page)).toHaveCount(0);
|
||||
},
|
||||
|
||||
async clickNextButton(page: Page) {
|
||||
await this.nextButton(page).click();
|
||||
},
|
||||
|
||||
async clickPreviousButton(page: Page) {
|
||||
await this.previousButton(page).click();
|
||||
},
|
||||
|
||||
async closeViewer(page: Page) {
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(this.locator(page)).not.toBeVisible();
|
||||
},
|
||||
|
||||
getCurrentAssetId(page: Page): string | null {
|
||||
const url = new URL(page.url());
|
||||
return getAssetIdFromUrl(url);
|
||||
},
|
||||
|
||||
async expectCurrentAssetId(page: Page, expectedAssetId: string) {
|
||||
await expect.poll(() => this.getCurrentAssetId(page)).toBe(expectedAssetId);
|
||||
},
|
||||
};
|
||||
@@ -68,7 +68,11 @@
|
||||
let currentMemoryAssetFull = $derived.by(async () =>
|
||||
current?.asset ? await getAssetInfo({ ...authManager.params, id: current.asset.id }) : undefined,
|
||||
);
|
||||
let currentTimelineAssets = $derived(current?.memory.assets || []);
|
||||
let currentTimelineAssets = $derived([
|
||||
...(current?.previousMemory?.assets ?? []),
|
||||
...(current?.memory.assets ?? []),
|
||||
...(current?.nextMemory?.assets ?? []),
|
||||
]);
|
||||
|
||||
let isSaved = $derived(current?.memory.isSaved);
|
||||
let viewerHeight = $state(0);
|
||||
|
||||
Reference in New Issue
Block a user