import { faker } from '@faker-js/faker'; import { expect, test } from '@playwright/test'; import { DateTime } from 'luxon'; import { Changes, createDefaultTimelineConfig, generateTimelineData, getAsset, getMockAsset, SeededRandom, selectRandom, selectRandomMultiple, TimelineAssetConfig, TimelineData, } from 'src/generators/timeline'; import { setupBaseMockApiRoutes } from 'src/mock-network/base-network'; import { pageRoutePromise, setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network'; import { utils } from 'src/utils'; import { assetViewerUtils, cancelAllPollers, padYearMonth, pageUtils, poll, thumbnailUtils, timelineUtils, } from 'src/web/specs/timeline/utils'; test.describe.configure({ mode: 'parallel' }); test.describe('Timeline', () => { let adminUserId: string; let timelineRestData: TimelineData; const assets: TimelineAssetConfig[] = []; const yearMonths: string[] = []; const testContext = new TimelineTestContext(); const changes: Changes = { albumAdditions: [], assetDeletions: [], assetArchivals: [], assetFavorites: [], }; test.beforeAll(async () => { test.fail( process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS !== '1', 'This test requires env var: PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1', ); utils.initSdk(); adminUserId = faker.string.uuid(); testContext.adminId = adminUserId; timelineRestData = generateTimelineData({ ...createDefaultTimelineConfig(), ownerId: adminUserId }); for (const timeBucket of timelineRestData.buckets.values()) { assets.push(...timeBucket); } for (const yearMonth of timelineRestData.buckets.keys()) { const [year, month] = yearMonth.split('-'); yearMonths.push(`${year}-${Number(month)}`); } }); test.beforeEach(async ({ context }) => { await setupBaseMockApiRoutes(context, adminUserId); await setupTimelineMockApiRoutes(context, timelineRestData, changes, testContext); }); test.afterEach(() => { cancelAllPollers(); testContext.slowBucket = false; changes.albumAdditions = []; changes.assetDeletions = []; changes.assetArchivals = []; changes.assetFavorites = []; }); test.describe('/photos', () => { test('Open /photos', async ({ page }) => { await page.goto(`/photos`); await page.waitForSelector('#asset-grid'); await thumbnailUtils.expectTimelineHasOnScreenAssets(page); }); test('Deep link to last photo', async ({ page }) => { const lastAsset = assets.at(-1)!; await pageUtils.deepLinkPhotosPage(page, lastAsset.id); await thumbnailUtils.expectTimelineHasOnScreenAssets(page); await thumbnailUtils.expectInViewport(page, lastAsset.id); }); const rng = new SeededRandom(529); for (let i = 0; i < 10; i++) { test('Deep link to random asset ' + i, async ({ page }) => { const asset = selectRandom(assets, rng); await pageUtils.deepLinkPhotosPage(page, asset.id); await thumbnailUtils.expectTimelineHasOnScreenAssets(page); await thumbnailUtils.expectInViewport(page, asset.id); }); } test('Open /photos, open asset-viewer, browser back', async ({ page }) => { const rng = new SeededRandom(22); const asset = selectRandom(assets, rng); await pageUtils.deepLinkPhotosPage(page, asset.id); const scrollTopBefore = await timelineUtils.getScrollTop(page); await thumbnailUtils.clickAssetId(page, asset.id); await assetViewerUtils.waitForViewerLoad(page, asset); await page.goBack(); await timelineUtils.locator(page).waitFor(); const scrollTopAfter = await timelineUtils.getScrollTop(page); expect(scrollTopAfter).toBe(scrollTopBefore); }); test('Open /photos, open asset-viewer, next photo, browser back, back', async ({ page }) => { const rng = new SeededRandom(49); const asset = selectRandom(assets, rng); const assetIndex = assets.indexOf(asset); const nextAsset = assets[assetIndex + 1]; await pageUtils.deepLinkPhotosPage(page, asset.id); const scrollTopBefore = await timelineUtils.getScrollTop(page); await thumbnailUtils.clickAssetId(page, asset.id); await assetViewerUtils.waitForViewerLoad(page, asset); await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${asset.id}`); await page.getByLabel('View next asset').click(); await assetViewerUtils.waitForViewerLoad(page, nextAsset); await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${nextAsset.id}`); await page.goBack(); await assetViewerUtils.waitForViewerLoad(page, asset); await page.goBack(); await page.waitForURL('**/photos?at=*'); const scrollTopAfter = await timelineUtils.getScrollTop(page); expect(Math.abs(scrollTopAfter - scrollTopBefore)).toBeLessThan(5); }); test('Open /photos, open asset-viewer, next photo 15x, backwardsArrow', async ({ page }) => { await pageUtils.deepLinkPhotosPage(page, assets[0].id); await thumbnailUtils.clickAssetId(page, assets[0].id); await assetViewerUtils.waitForViewerLoad(page, assets[0]); for (let i = 1; i <= 15; i++) { await page.getByLabel('View next asset').click(); await assetViewerUtils.waitForViewerLoad(page, assets[i]); } await page.getByLabel('Go back').click(); await page.waitForURL('**/photos?at=*'); await thumbnailUtils.expectInViewport(page, assets[15].id); await thumbnailUtils.expectBottomIsTimelineBottom(page, assets[15]!.id); }); test('Open /photos, open asset-viewer, previous photo 15x, backwardsArrow', async ({ page }) => { const lastAsset = assets.at(-1)!; await pageUtils.deepLinkPhotosPage(page, lastAsset.id); await thumbnailUtils.clickAssetId(page, lastAsset.id); await assetViewerUtils.waitForViewerLoad(page, lastAsset); for (let i = 1; i <= 15; i++) { await page.getByLabel('View previous asset').click(); await assetViewerUtils.waitForViewerLoad(page, assets.at(-1 - i)!); } await page.getByLabel('Go back').click(); await page.waitForURL('**/photos?at=*'); await thumbnailUtils.expectInViewport(page, assets.at(-1 - 15)!.id); await thumbnailUtils.expectTopIsTimelineTop(page, assets.at(-1 - 15)!.id); }); }); test.describe('keyboard', () => { /** * This text tests keyboard nativation, and also ensures that the scroll-to-asset behavior * scrolls the minimum amount. That is, if you are navigating using right arrow (auto scrolling * as necessary downwards), then the asset should always be at the lowest row of the grid. */ test('Next/previous asset - ArrowRight/ArrowLeft', async ({ page }) => { await pageUtils.openPhotosPage(page); await thumbnailUtils.withAssetId(page, assets[0].id).focus(); const rightKey = 'ArrowRight'; const leftKey = 'ArrowLeft'; for (let i = 1; i < 15; i++) { await page.keyboard.press(rightKey); await assetViewerUtils.expectActiveAssetToBe(page, assets[i].id); } for (let i = 15; i <= 20; i++) { await page.keyboard.press(rightKey); await assetViewerUtils.expectActiveAssetToBe(page, assets[i].id); expect(await thumbnailUtils.expectBottomIsTimelineBottom(page, assets.at(i)!.id)); } // now test previous asset for (let i = 19; i >= 15; i--) { await page.keyboard.press(leftKey); await assetViewerUtils.expectActiveAssetToBe(page, assets[i].id); } for (let i = 14; i > 0; i--) { await page.keyboard.press(leftKey); await assetViewerUtils.expectActiveAssetToBe(page, assets[i].id); expect(await thumbnailUtils.expectTopIsTimelineTop(page, assets.at(i)!.id)); } }); test('Next/previous asset - Tab/Shift+Tab', async ({ page }) => { await pageUtils.openPhotosPage(page); await thumbnailUtils.withAssetId(page, assets[0].id).focus(); const rightKey = 'Tab'; const leftKey = 'Shift+Tab'; for (let i = 1; i < 15; i++) { await page.keyboard.press(rightKey); await assetViewerUtils.expectActiveAssetToBe(page, assets[i].id); } for (let i = 15; i <= 20; i++) { await page.keyboard.press(rightKey); await assetViewerUtils.expectActiveAssetToBe(page, assets[i].id); } // now test previous asset for (let i = 19; i >= 15; i--) { await page.keyboard.press(leftKey); await assetViewerUtils.expectActiveAssetToBe(page, assets[i].id); } for (let i = 14; i > 0; i--) { await page.keyboard.press(leftKey); await assetViewerUtils.expectActiveAssetToBe(page, assets[i].id); } }); test('Next/previous day - d, Shift+D', async ({ page }) => { await pageUtils.openPhotosPage(page); let asset = assets[0]; await timelineUtils.locator(page).hover(); await page.keyboard.press('d'); await assetViewerUtils.expectActiveAssetToBe(page, asset.id); for (let i = 0; i < 15; i++) { await page.keyboard.press('d'); const next = getMockAsset(asset, assets, 'next', 'day')!; await assetViewerUtils.expectActiveAssetToBe(page, next.id); asset = next; } for (let i = 0; i < 15; i++) { await page.keyboard.press('Shift+D'); const previous = getMockAsset(asset, assets, 'previous', 'day')!; await assetViewerUtils.expectActiveAssetToBe(page, previous.id); asset = previous; } }); test('Next/previous month - m, Shift+M', async ({ page }) => { await pageUtils.openPhotosPage(page); let asset = assets[0]; await timelineUtils.locator(page).hover(); await page.keyboard.press('m'); await assetViewerUtils.expectActiveAssetToBe(page, asset.id); for (let i = 0; i < 15; i++) { await page.keyboard.press('m'); const next = getMockAsset(asset, assets, 'next', 'month')!; await assetViewerUtils.expectActiveAssetToBe(page, next.id); asset = next; } for (let i = 0; i < 15; i++) { await page.keyboard.press('Shift+M'); const previous = getMockAsset(asset, assets, 'previous', 'month')!; await assetViewerUtils.expectActiveAssetToBe(page, previous.id); asset = previous; } }); test('Next/previous year - y, Shift+Y', async ({ page }) => { await pageUtils.openPhotosPage(page); let asset = assets[0]; await timelineUtils.locator(page).hover(); await page.keyboard.press('y'); await assetViewerUtils.expectActiveAssetToBe(page, asset.id); for (let i = 0; i < 15; i++) { await page.keyboard.press('y'); const next = getMockAsset(asset, assets, 'next', 'year')!; await assetViewerUtils.expectActiveAssetToBe(page, next.id); asset = next; } for (let i = 0; i < 15; i++) { await page.keyboard.press('Shift+Y'); const previous = getMockAsset(asset, assets, 'previous', 'year')!; await assetViewerUtils.expectActiveAssetToBe(page, previous.id); asset = previous; } }); test('Navigate to time - g', async ({ page }) => { const rng = new SeededRandom(4782); await pageUtils.openPhotosPage(page); for (let i = 0; i < 10; i++) { const asset = selectRandom(assets, rng); await pageUtils.goToAsset(page, asset.fileCreatedAt); await thumbnailUtils.expectInViewport(page, asset.id); } }); }); test.describe('selection', () => { test('Select day, unselect day', async ({ page }) => { await pageUtils.openPhotosPage(page); await pageUtils.selectDay(page, 'Wed, Dec 11, 2024'); await expect(thumbnailUtils.selectedAsset(page)).toHaveCount(4); await pageUtils.selectDay(page, 'Wed, Dec 11, 2024'); await expect(thumbnailUtils.selectedAsset(page)).toHaveCount(0); }); test('Select asset, click asset to select', async ({ page }) => { await pageUtils.openPhotosPage(page); await thumbnailUtils.withAssetId(page, assets[1].id).hover(); await thumbnailUtils.selectButton(page, assets[1].id).click(); await expect(thumbnailUtils.selectedAsset(page)).toHaveCount(1); // no need to hover, once selection is active await thumbnailUtils.clickAssetId(page, assets[2].id); await expect(thumbnailUtils.selectedAsset(page)).toHaveCount(2); }); test('Select asset, click unselect asset', async ({ page }) => { await pageUtils.openPhotosPage(page); await thumbnailUtils.withAssetId(page, assets[1].id).hover(); await thumbnailUtils.selectButton(page, assets[1].id).click(); await expect(thumbnailUtils.selectedAsset(page)).toHaveCount(1); await thumbnailUtils.clickAssetId(page, assets[1].id); // the hover uses a checked button too, so just move mouse away await page.mouse.move(0, 0); await expect(thumbnailUtils.selectedAsset(page)).toHaveCount(0); }); test('Select asset, shift-hover candidates, shift-click end', async ({ page }) => { await pageUtils.openPhotosPage(page); const asset = assets[0]; await thumbnailUtils.withAssetId(page, asset.id).hover(); await thumbnailUtils.selectButton(page, asset.id).click(); await page.keyboard.down('Shift'); await thumbnailUtils.withAssetId(page, assets[2].id).hover(); await expect( thumbnailUtils.locator(page).locator('.absolute.top-0.h-full.w-full.bg-immich-primary.opacity-40'), ).toHaveCount(3); await thumbnailUtils.selectButton(page, assets[2].id).click(); await page.keyboard.up('Shift'); await expect(thumbnailUtils.selectedAsset(page)).toHaveCount(3); }); test('Add multiple to selection - Select day, shift-click end', async ({ page }) => { await pageUtils.openPhotosPage(page); await thumbnailUtils.withAssetId(page, assets[0].id).hover(); await thumbnailUtils.selectButton(page, assets[0].id).click(); await thumbnailUtils.clickAssetId(page, assets[2].id); await page.keyboard.down('Shift'); await thumbnailUtils.clickAssetId(page, assets[4].id); await page.mouse.move(0, 0); await expect(thumbnailUtils.selectedAsset(page)).toHaveCount(4); }); }); test.describe('scroll', () => { test('Open /photos, random click scrubber 20x', async ({ page }) => { test.slow(); await pageUtils.openPhotosPage(page); const rng = new SeededRandom(6637); const selectedMonths = selectRandomMultiple(yearMonths, 20, rng); for (const month of selectedMonths) { await page.locator(`[data-segment-year-month="${month}"]`).click({ force: true }); const visibleMockAssetsYearMonths = await poll(page, async () => { const assetIds = await thumbnailUtils.getAllInViewport( page, (assetId: string) => getYearMonth(assets, assetId) === month, ); const visibleMockAssetsYearMonths: string[] = []; for (const assetId of assetIds!) { const yearMonth = getYearMonth(assets, assetId); visibleMockAssetsYearMonths.push(yearMonth); if (yearMonth === month) { return [yearMonth]; } } }); if (page.isClosed()) { return; } expect(visibleMockAssetsYearMonths).toContain(month); } }); test('Deep link to last photo, scroll up', async ({ page }) => { const lastAsset = assets.at(-1)!; await pageUtils.deepLinkPhotosPage(page, lastAsset.id); await timelineUtils.locator(page).hover(); for (let i = 0; i < 100; i++) { await page.mouse.wheel(0, -100); await page.waitForTimeout(25); } await thumbnailUtils.expectInViewport(page, '14e5901f-fd7f-40c0-b186-4d7e7fc67968'); }); test('Deep link to first bucket, scroll down', async ({ page }) => { const lastAsset = assets.at(0)!; await pageUtils.deepLinkPhotosPage(page, lastAsset.id); await timelineUtils.locator(page).hover(); for (let i = 0; i < 100; i++) { await page.mouse.wheel(0, 100); await page.waitForTimeout(25); } await thumbnailUtils.expectInViewport(page, 'b7983a13-4b4e-4950-a731-f2962d9a1555'); }); test('Deep link to last photo, drag scrubber to scroll up', async ({ page }) => { const lastAsset = assets.at(-1)!; await pageUtils.deepLinkPhotosPage(page, lastAsset.id); const lastMonth = yearMonths.at(-1); const firstScrubSegment = page.locator(`[data-segment-year-month="${yearMonths[0]}"]`); const lastScrubSegment = page.locator(`[data-segment-year-month="${lastMonth}"]`); const sourcebox = (await lastScrubSegment.boundingBox())!; const targetBox = (await firstScrubSegment.boundingBox())!; await firstScrubSegment.hover(); const currentY = sourcebox.y; await page.mouse.move(sourcebox.x + sourcebox?.width / 2, currentY); await page.mouse.down(); await page.mouse.move(sourcebox.x + sourcebox?.width / 2, targetBox.y, { steps: 100 }); await page.mouse.up(); await thumbnailUtils.expectInViewport(page, assets[0].id); }); test('Deep link to first bucket, drag scrubber to scroll down', async ({ page }) => { await pageUtils.deepLinkPhotosPage(page, assets[0].id); const firstScrubSegment = page.locator(`[data-segment-year-month="${yearMonths[0]}"]`); const sourcebox = (await firstScrubSegment.boundingBox())!; await firstScrubSegment.hover(); const currentY = sourcebox.y; await page.mouse.move(sourcebox.x + sourcebox?.width / 2, currentY); await page.mouse.down(); const height = page.viewportSize()?.height; expect(height).toBeDefined(); await page.mouse.move(sourcebox.x + sourcebox?.width / 2, height! - 10, { steps: 100, }); await page.mouse.up(); await thumbnailUtils.expectInViewport(page, assets.at(-1)!.id); }); test('Buckets cancel on scroll', async ({ page }) => { await pageUtils.openPhotosPage(page); testContext.slowBucket = true; const failedUris: string[] = []; page.on('requestfailed', (request) => { failedUris.push(request.url()); }); const offscreenSegment = page.locator(`[data-segment-year-month="${yearMonths[12]}"]`); await offscreenSegment.click({ force: true }); const lastSegment = page.locator(`[data-segment-year-month="${yearMonths.at(-1)!}"]`); await lastSegment.click({ force: true }); const uris = await poll(page, async () => (failedUris.length > 0 ? failedUris : null)); expect(uris).toEqual(expect.arrayContaining([expect.stringContaining(padYearMonth(yearMonths[12]!))])); }); }); test.describe('/albums', () => { test('Open album', async ({ page }) => { const album = timelineRestData.album; await pageUtils.openAlbumPage(page, album.id); await thumbnailUtils.expectInViewport(page, album.assetIds[0]); }); test('Deep link to last photo', async ({ page }) => { const album = timelineRestData.album; const lastAsset = album.assetIds.at(-1); await pageUtils.deepLinkAlbumPage(page, album.id, lastAsset!); await thumbnailUtils.expectInViewport(page, album.assetIds.at(-1)!); await thumbnailUtils.expectBottomIsTimelineBottom(page, album.assetIds.at(-1)!); }); test('Add photos to album pre-selects existing', async ({ page }) => { const album = timelineRestData.album; await pageUtils.openAlbumPage(page, album.id); await page.getByLabel('Add photos').click(); const asset = getAsset(timelineRestData, album.assetIds[0])!; await pageUtils.goToAsset(page, asset.fileCreatedAt); await thumbnailUtils.expectInViewport(page, asset.id); await thumbnailUtils.expectSelectedReadonly(page, asset.id); }); test('Add photos to album', async ({ page }) => { const album = timelineRestData.album; await pageUtils.openAlbumPage(page, album.id); await page.locator('nav button[aria-label="Add photos"]').click(); const asset = getAsset(timelineRestData, album.assetIds[0])!; await pageUtils.goToAsset(page, asset.fileCreatedAt); await thumbnailUtils.expectInViewport(page, asset.id); await thumbnailUtils.expectSelectedReadonly(page, asset.id); await pageUtils.selectDay(page, 'Tue, Feb 27, 2024'); const put = pageRoutePromise(page, `**/api/albums/${album.id}/assets`, async (route, request) => { const requestJson = request.postDataJSON(); await route.fulfill({ status: 200, contentType: 'application/json', json: requestJson.ids.map((id: string) => ({ id, success: true })), }); changes.albumAdditions.push(...requestJson.ids); }); await page.getByText('Done').click(); await expect(put).resolves.toEqual({ ids: [ 'c077ea7b-cfa1-45e4-8554-f86c00ee5658', '040fd762-dbbc-486d-a51a-2d84115e6229', '86af0b5f-79d3-4f75-bab3-3b61f6c72b23', ], }); const addedAsset = getAsset(timelineRestData, 'c077ea7b-cfa1-45e4-8554-f86c00ee5658')!; await pageUtils.goToAsset(page, addedAsset.fileCreatedAt); await thumbnailUtils.expectInViewport(page, 'c077ea7b-cfa1-45e4-8554-f86c00ee5658'); await thumbnailUtils.expectInViewport(page, '040fd762-dbbc-486d-a51a-2d84115e6229'); await thumbnailUtils.expectInViewport(page, '86af0b5f-79d3-4f75-bab3-3b61f6c72b23'); }); }); test.describe('/trash', () => { test('open /photos, trash photo, open /trash, restore', async ({ page }) => { await pageUtils.openPhotosPage(page); const assetToTrash = assets[0]; await thumbnailUtils.withAssetId(page, assetToTrash.id).hover(); await thumbnailUtils.selectButton(page, assetToTrash.id).click(); await page.getByLabel('Menu').click(); const deleteRequest = pageRoutePromise(page, '**/api/assets', async (route, request) => { const requestJson = request.postDataJSON(); changes.assetDeletions.push(...requestJson.ids); await route.fulfill({ status: 200, contentType: 'application/json', json: requestJson.ids.map((id: string) => ({ id, success: true })), }); }); await page.getByRole('menuitem').getByText('Delete').click(); await expect(deleteRequest).resolves.toEqual({ force: false, ids: [assetToTrash.id], }); await page.getByText('Trash', { exact: true }).click(); await thumbnailUtils.expectInViewport(page, assetToTrash.id); await thumbnailUtils.withAssetId(page, assetToTrash.id).hover(); await thumbnailUtils.selectButton(page, assetToTrash.id).click(); const restoreRequest = pageRoutePromise(page, '**/api/trash/restore/assets', async (route, request) => { const requestJson = request.postDataJSON(); changes.assetDeletions = changes.assetDeletions.filter((id) => !requestJson.ids.includes(id)); await route.fulfill({ status: 200, contentType: 'application/json', json: { count: requestJson.ids.length }, }); }); await page.getByText('Restore', { exact: true }).click(); await expect(restoreRequest).resolves.toEqual({ ids: [assetToTrash.id], }); await expect(thumbnailUtils.withAssetId(page, assetToTrash.id)).toHaveCount(0); await page.getByText('Photos', { exact: true }).click(); await thumbnailUtils.expectInViewport(page, assetToTrash.id); }); test('open album, trash photo, open /trash, restore', async ({ page }) => { const album = timelineRestData.album; await pageUtils.openAlbumPage(page, album.id); const assetToTrash = getAsset(timelineRestData, album.assetIds[0])!; await thumbnailUtils.withAssetId(page, assetToTrash.id).hover(); await thumbnailUtils.selectButton(page, assetToTrash.id).click(); await page.getByLabel('Menu').click(); const deleteRequest = pageRoutePromise(page, '**/api/assets', async (route, request) => { const requestJson = request.postDataJSON(); changes.assetDeletions.push(...requestJson.ids); await route.fulfill({ status: 200, contentType: 'application/json', json: requestJson.ids.map((id: string) => ({ id, success: true })), }); }); await page.getByRole('menuitem').getByText('Delete').click(); await expect(deleteRequest).resolves.toEqual({ force: false, ids: [assetToTrash.id], }); await page.locator('#asset-selection-app-bar').getByLabel('Close').click(); await page.getByText('Trash', { exact: true }).click(); await timelineUtils.waitForTimelineLoad(page); await thumbnailUtils.expectInViewport(page, assetToTrash.id); await thumbnailUtils.withAssetId(page, assetToTrash.id).hover(); await thumbnailUtils.selectButton(page, assetToTrash.id).click(); const restoreRequest = pageRoutePromise(page, '**/api/trash/restore/assets', async (route, request) => { const requestJson = request.postDataJSON(); changes.assetDeletions = changes.assetDeletions.filter((id) => !requestJson.ids.includes(id)); await route.fulfill({ status: 200, contentType: 'application/json', json: { count: requestJson.ids.length }, }); }); await page.getByText('Restore', { exact: true }).click(); await expect(restoreRequest).resolves.toEqual({ ids: [assetToTrash.id], }); await expect(thumbnailUtils.withAssetId(page, assetToTrash.id)).toHaveCount(0); await pageUtils.openAlbumPage(page, album.id); await thumbnailUtils.expectInViewport(page, assetToTrash.id); }); }); test.describe('/archive', () => { test('open /photos, archive photo, open /archive, unarchive', async ({ page }) => { await pageUtils.openPhotosPage(page); const assetToArchive = assets[0]; await thumbnailUtils.withAssetId(page, assetToArchive.id).hover(); await thumbnailUtils.selectButton(page, assetToArchive.id).click(); await page.getByLabel('Menu').click(); const archive = pageRoutePromise(page, '**/api/assets', async (route, request) => { const requestJson = request.postDataJSON(); if (requestJson.visibility !== 'archive') { return await route.continue(); } await route.fulfill({ status: 204, }); changes.assetArchivals.push(...requestJson.ids); }); await page.getByRole('menuitem').getByText('Archive').click(); await expect(archive).resolves.toEqual({ visibility: 'archive', ids: [assetToArchive.id], }); await expect(thumbnailUtils.withAssetId(page, assetToArchive.id)).toHaveCount(0); await page.getByRole('link').getByText('Archive').click(); await thumbnailUtils.expectInViewport(page, assetToArchive.id); await thumbnailUtils.withAssetId(page, assetToArchive.id).hover(); await thumbnailUtils.selectButton(page, assetToArchive.id).click(); const unarchiveRequest = pageRoutePromise(page, '**/api/assets', async (route, request) => { const requestJson = request.postDataJSON(); if (requestJson.visibility !== 'timeline') { return await route.continue(); } changes.assetArchivals = changes.assetArchivals.filter((id) => !requestJson.ids.includes(id)); await route.fulfill({ status: 204, }); }); await page.getByLabel('Unarchive').click(); await expect(unarchiveRequest).resolves.toEqual({ visibility: 'timeline', ids: [assetToArchive.id], }); console.log('Skipping assertion - TODO - fix bug with not removing asset from timeline-manager after unarchive'); // await expect(thumbnail.withAssetId(page, assetToArchive.id)).toHaveCount(0); await page.getByText('Photos', { exact: true }).click(); await thumbnailUtils.expectInViewport(page, assetToArchive.id); }); test('open album, archive photo, open album, unarchive', async ({ page }) => { const album = timelineRestData.album; await pageUtils.openAlbumPage(page, album.id); const assetToArchive = getAsset(timelineRestData, album.assetIds[0])!; await thumbnailUtils.withAssetId(page, assetToArchive.id).hover(); await thumbnailUtils.selectButton(page, assetToArchive.id).click(); await page.getByLabel('Menu').click(); const archive = pageRoutePromise(page, '**/api/assets', async (route, request) => { const requestJson = request.postDataJSON(); if (requestJson.visibility !== 'archive') { return await route.continue(); } changes.assetArchivals.push(...requestJson.ids); await route.fulfill({ status: 204, }); }); await page.getByRole('menuitem').getByText('Archive').click(); await expect(archive).resolves.toEqual({ visibility: 'archive', ids: [assetToArchive.id], }); console.log('Skipping assertion - TODO - fix that archiving in album doesnt add icon'); // await thumbnail.expectThumbnailIsArchive(page, assetToArchive.id); await page.locator('#asset-selection-app-bar').getByLabel('Close').click(); await page.getByRole('link').getByText('Archive').click(); await timelineUtils.waitForTimelineLoad(page); await thumbnailUtils.expectInViewport(page, assetToArchive.id); await thumbnailUtils.withAssetId(page, assetToArchive.id).hover(); await thumbnailUtils.selectButton(page, assetToArchive.id).click(); const unarchiveRequest = pageRoutePromise(page, '**/api/assets', async (route, request) => { const requestJson = request.postDataJSON(); if (requestJson.visibility !== 'timeline') { return await route.continue(); } changes.assetArchivals = changes.assetArchivals.filter((id) => !requestJson.ids.includes(id)); await route.fulfill({ status: 204, }); }); await page.getByLabel('Unarchive').click(); await expect(unarchiveRequest).resolves.toEqual({ visibility: 'timeline', ids: [assetToArchive.id], }); console.log('Skipping assertion - TODO - fix bug with not removing asset from timeline-manager after unarchive'); // await expect(thumbnail.withAssetId(page, assetToArchive.id)).toHaveCount(0); await pageUtils.openAlbumPage(page, album.id); await thumbnailUtils.expectInViewport(page, assetToArchive.id); }); }); test.describe('/favorite', () => { test('open /photos, favorite photo, open /favorites, remove favorite, open /photos', async ({ page }) => { await pageUtils.openPhotosPage(page); const assetToFavorite = assets[0]; await thumbnailUtils.withAssetId(page, assetToFavorite.id).hover(); await thumbnailUtils.selectButton(page, assetToFavorite.id).click(); const favorite = pageRoutePromise(page, '**/api/assets', async (route, request) => { const requestJson = request.postDataJSON(); if (requestJson.isFavorite === undefined) { return await route.continue(); } const isFavorite = requestJson.isFavorite; if (isFavorite) { changes.assetFavorites.push(...requestJson.ids); } await route.fulfill({ status: 204, }); }); await page.getByLabel('Favorite').click(); await expect(favorite).resolves.toEqual({ isFavorite: true, ids: [assetToFavorite.id], }); // ensure thumbnail still exists and has favorite icon await thumbnailUtils.expectThumbnailIsFavorite(page, assetToFavorite.id); await page.getByRole('link').getByText('Favorites').click(); await thumbnailUtils.expectInViewport(page, assetToFavorite.id); await thumbnailUtils.withAssetId(page, assetToFavorite.id).hover(); await thumbnailUtils.selectButton(page, assetToFavorite.id).click(); const unFavoriteRequest = pageRoutePromise(page, '**/api/assets', async (route, request) => { const requestJson = request.postDataJSON(); if (requestJson.isFavorite === undefined) { return await route.continue(); } changes.assetFavorites = changes.assetFavorites.filter((id) => !requestJson.ids.includes(id)); await route.fulfill({ status: 204, }); }); await page.getByLabel('Remove from favorites').click(); await expect(unFavoriteRequest).resolves.toEqual({ isFavorite: false, ids: [assetToFavorite.id], }); await expect(thumbnailUtils.withAssetId(page, assetToFavorite.id)).toHaveCount(0); await page.getByText('Photos', { exact: true }).click(); await thumbnailUtils.expectInViewport(page, assetToFavorite.id); }); test('Open album, favorite photo, open /favorites, remove favorite, Open album', async ({ page }) => { const album = timelineRestData.album; await pageUtils.openAlbumPage(page, album.id); const assetToFavorite = getAsset(timelineRestData, album.assetIds[0])!; await thumbnailUtils.withAssetId(page, assetToFavorite.id).hover(); await thumbnailUtils.selectButton(page, assetToFavorite.id).click(); const favorite = pageRoutePromise(page, '**/api/assets', async (route, request) => { const requestJson = request.postDataJSON(); if (requestJson.isFavorite === undefined) { return await route.continue(); } const isFavorite = requestJson.isFavorite; if (isFavorite) { changes.assetFavorites.push(...requestJson.ids); } await route.fulfill({ status: 204, }); }); await page.getByLabel('Favorite').click(); await expect(favorite).resolves.toEqual({ isFavorite: true, ids: [assetToFavorite.id], }); // ensure thumbnail still exists and has favorite icon await thumbnailUtils.expectThumbnailIsFavorite(page, assetToFavorite.id); await page.locator('#asset-selection-app-bar').getByLabel('Close').click(); await page.getByRole('link').getByText('Favorites').click(); await timelineUtils.waitForTimelineLoad(page); await pageUtils.goToAsset(page, assetToFavorite.fileCreatedAt); await thumbnailUtils.expectInViewport(page, assetToFavorite.id); await thumbnailUtils.withAssetId(page, assetToFavorite.id).hover(); await thumbnailUtils.selectButton(page, assetToFavorite.id).click(); const unFavoriteRequest = pageRoutePromise(page, '**/api/assets', async (route, request) => { const requestJson = request.postDataJSON(); if (requestJson.isFavorite === undefined) { return await route.continue(); } changes.assetFavorites = changes.assetFavorites.filter((id) => !requestJson.ids.includes(id)); await route.fulfill({ status: 204, }); }); await page.getByLabel('Remove from favorites').click(); await expect(unFavoriteRequest).resolves.toEqual({ isFavorite: false, ids: [assetToFavorite.id], }); await expect(thumbnailUtils.withAssetId(page, assetToFavorite.id)).toHaveCount(0); await pageUtils.openAlbumPage(page, album.id); await thumbnailUtils.expectInViewport(page, assetToFavorite.id); }); }); }); const getYearMonth = (assets: TimelineAssetConfig[], assetId: string) => { const mockAsset = assets.find((mockAsset) => mockAsset.id === assetId)!; const dateTime = DateTime.fromISO(mockAsset.fileCreatedAt!); return dateTime.year + '-' + dateTime.month; };