mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
8764a1894b
* feat(web): adaptive progressive image loading for photo viewer Replace ImageManager with a new AdaptiveImageLoader that progressively loads images through quality tiers (thumbnail → preview → original). New components and utilities: - AdaptiveImage: layered image renderer with thumbhash, thumbnail, preview, and original layers with visibility managed by load state - AdaptiveImageLoader: state machine driving the quality progression with per-quality callbacks and error handling - ImageLayer/Image: low-level image elements with load/error lifecycle - PreloadManager: preloads adjacent assets for instant navigation - AlphaBackground/DelayedLoadingSpinner: loading state UI Zoom is handled via a derived CSS transform applied to the content wrapper in AdaptiveImage, with the zoom library (zoomTarget: null) only tracking state without manipulating the DOM directly. Also adds scaleToCover to container-utils and getAssetUrls to utils. * fix: don't partially render images in firefox * add passive loading indicator to asset-viewer --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
87 lines
3.1 KiB
TypeScript
87 lines
3.1 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
import { toAssetResponseDto } from 'src/ui/generators/timeline';
|
|
import {
|
|
createMockStack,
|
|
createMockStackAsset,
|
|
MockStack,
|
|
setupBrokenAssetMockApiRoutes,
|
|
} from 'src/ui/mock-network/broken-asset-network';
|
|
import { assetViewerUtils } from '../timeline/utils';
|
|
import { setupAssetViewerFixture } from './utils';
|
|
|
|
test.describe.configure({ mode: 'parallel' });
|
|
test.describe('broken-asset responsiveness', () => {
|
|
const fixture = setupAssetViewerFixture(889);
|
|
let mockStack: MockStack;
|
|
|
|
test.beforeAll(async () => {
|
|
const primaryAssetDto = toAssetResponseDto(fixture.primaryAsset);
|
|
|
|
const brokenAssets = [
|
|
createMockStackAsset(fixture.adminUserId),
|
|
createMockStackAsset(fixture.adminUserId),
|
|
createMockStackAsset(fixture.adminUserId),
|
|
];
|
|
|
|
mockStack = createMockStack(primaryAssetDto, brokenAssets);
|
|
});
|
|
|
|
test.beforeEach(async ({ context }) => {
|
|
await setupBrokenAssetMockApiRoutes(context, mockStack);
|
|
});
|
|
|
|
test('broken asset in stack strip hides icon at small size', async ({ page }) => {
|
|
await page.goto(`/photos/${fixture.primaryAsset.id}`);
|
|
await assetViewerUtils.waitForViewerLoad(page, fixture.primaryAsset);
|
|
|
|
const stackSlideshow = page.locator('#stack-slideshow');
|
|
await expect(stackSlideshow).toBeVisible();
|
|
|
|
const brokenAssets = stackSlideshow.locator('[data-broken-asset]');
|
|
await expect(brokenAssets.first()).toBeVisible();
|
|
await expect(brokenAssets).toHaveCount(mockStack.brokenAssetIds.size);
|
|
|
|
for (const brokenAsset of await brokenAssets.all()) {
|
|
await expect(brokenAsset.locator('svg')).not.toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('broken asset in stack strip uses text-xs class', async ({ page }) => {
|
|
await page.goto(`/photos/${fixture.primaryAsset.id}`);
|
|
await assetViewerUtils.waitForViewerLoad(page, fixture.primaryAsset);
|
|
|
|
const stackSlideshow = page.locator('#stack-slideshow');
|
|
await expect(stackSlideshow).toBeVisible();
|
|
|
|
const brokenAssets = stackSlideshow.locator('[data-broken-asset]');
|
|
await expect(brokenAssets.first()).toBeVisible();
|
|
|
|
for (const brokenAsset of await brokenAssets.all()) {
|
|
const messageSpan = brokenAsset.locator('span');
|
|
await expect(messageSpan).toHaveClass(/text-xs/);
|
|
}
|
|
});
|
|
|
|
test('broken asset in main viewer shows icon and uses text-base', async ({ context, page }) => {
|
|
await context.route(
|
|
(url) =>
|
|
url.pathname.includes(`/api/assets/${fixture.primaryAsset.id}/thumbnail`) ||
|
|
url.pathname.includes(`/api/assets/${fixture.primaryAsset.id}/original`),
|
|
async (route) => {
|
|
return route.fulfill({ status: 404 });
|
|
},
|
|
);
|
|
|
|
await page.goto(`/photos/${fixture.primaryAsset.id}`);
|
|
await page.waitForSelector('#immich-asset-viewer');
|
|
|
|
const viewerBrokenAsset = page.locator('[data-viewer-content] [data-broken-asset]').first();
|
|
await expect(viewerBrokenAsset).toBeVisible();
|
|
|
|
await expect(viewerBrokenAsset.locator('svg')).toBeVisible();
|
|
|
|
const messageSpan = viewerBrokenAsset.locator('span');
|
|
await expect(messageSpan).toHaveClass(/text-base/);
|
|
});
|
|
});
|