Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
"dbaeumer.vscode-eslint",
"dart-code.flutter",
"dart-code.dart-code",
"dcmdev.dcm-vscode-extension"
"dcmdev.dcm-vscode-extension",
"bradlc.vscode-tailwindcss",
"ms-playwright.playwright",
"vitest.explorer",
"editorconfig.editorconfig",
"foxundermoon.shell-format",
"timonwong.shellcheck",
"bluebrown.yamlfmt"
]
}
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Please try to keep pull requests as focused as possible. A PR should do exactly

If you are looking for something to work on, there are discussions and issues with a `good-first-issue` label on them. These are always a good starting point. If none of them sound interesting or fit your skill set, feel free to reach out on our Discord. We're happy to help you find something to work on!

We usually do not assign issues to new contributors, since it happens often that a PR is never even opened. Again, reach out on Discord if you fear putting a lot of time into fixing an issue, but ending up with a duplicate PR.

## Use of generative AI

We ask you not to open PRs generated with an LLM. We find that code generated like this tends to need a large amount of back-and-forth, which is a very inefficient use of our time. If we want LLM-generated code, it's much faster for us to use an LLM ourselves than to go through an intermediary via a pull request.
Expand Down
4 changes: 2 additions & 2 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1",
"@types/node": "^24.10.14",
"@vitest/coverage-v8": "^3.0.0",
"@vitest/coverage-v8": "^4.0.0",
"byte-size": "^9.0.0",
"cli-progress": "^3.12.0",
"commander": "^12.0.0",
Expand All @@ -37,7 +37,7 @@
"typescript-eslint": "^8.28.0",
"vite": "^7.0.0",
"vite-tsconfig-paths": "^6.0.0",
"vitest": "^3.0.0",
"vitest": "^4.0.0",
"vitest-fetch-mock": "^0.4.0",
"yaml": "^2.3.1"
},
Expand Down
81 changes: 45 additions & 36 deletions cli/src/commands/asset.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { setTimeout as sleep } from 'node:timers/promises';
import { describe, expect, it, MockedFunction, vi } from 'vitest';

Expand Down Expand Up @@ -58,7 +58,7 @@ describe('uploadFiles', () => {
});

it('returns new assets when upload file is successful', async () => {
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), () => {
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), function () {
return {
status: 200,
body: JSON.stringify({ id: 'fc5621b1-86f6-44a1-9905-403e607df9f5', status: 'created' }),
Expand All @@ -75,7 +75,7 @@ describe('uploadFiles', () => {

it('returns new assets when upload file retry is successful', async () => {
let counter = 0;
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), () => {
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), function () {
counter++;
if (counter < retry) {
throw new Error('Network error');
Expand All @@ -96,7 +96,7 @@ describe('uploadFiles', () => {
});

it('returns new assets when upload file retry is failed', async () => {
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), () => {
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), function () {
throw new Error('Network error');
});

Expand Down Expand Up @@ -236,16 +236,19 @@ describe('startWatch', () => {
await sleep(100); // to debounce the watcher from considering the test file as a existing file
await fs.promises.writeFile(testFilePath, 'testjpg');

await vi.waitUntil(() => checkBulkUploadMocked.mock.calls.length > 0, 3000);
expect(checkBulkUpload).toHaveBeenCalledWith({
assetBulkUploadCheckDto: {
assets: [
expect.objectContaining({
id: testFilePath,
}),
],
},
});
await vi.waitFor(
() =>
expect(checkBulkUpload).toHaveBeenCalledWith({
assetBulkUploadCheckDto: {
assets: [
expect.objectContaining({
id: testFilePath,
}),
],
},
}),
{ timeout: 5000 },
);
});

it('should filter out unsupported files', async () => {
Expand All @@ -257,16 +260,19 @@ describe('startWatch', () => {
await fs.promises.writeFile(testFilePath, 'testjpg');
await fs.promises.writeFile(unsupportedFilePath, 'testtxt');

await vi.waitUntil(() => checkBulkUploadMocked.mock.calls.length > 0, 3000);
expect(checkBulkUpload).toHaveBeenCalledWith({
assetBulkUploadCheckDto: {
assets: expect.arrayContaining([
expect.objectContaining({
id: testFilePath,
}),
]),
},
});
await vi.waitFor(
() =>
expect(checkBulkUpload).toHaveBeenCalledWith({
assetBulkUploadCheckDto: {
assets: expect.arrayContaining([
expect.objectContaining({
id: testFilePath,
}),
]),
},
}),
{ timeout: 5000 },
);

expect(checkBulkUpload).not.toHaveBeenCalledWith({
assetBulkUploadCheckDto: {
Expand All @@ -291,16 +297,19 @@ describe('startWatch', () => {
await fs.promises.writeFile(testFilePath, 'testjpg');
await fs.promises.writeFile(ignoredFilePath, 'ignoredjpg');

await vi.waitUntil(() => checkBulkUploadMocked.mock.calls.length > 0, 3000);
expect(checkBulkUpload).toHaveBeenCalledWith({
assetBulkUploadCheckDto: {
assets: expect.arrayContaining([
expect.objectContaining({
id: testFilePath,
}),
]),
},
});
await vi.waitFor(
() =>
expect(checkBulkUpload).toHaveBeenCalledWith({
assetBulkUploadCheckDto: {
assets: expect.arrayContaining([
expect.objectContaining({
id: testFilePath,
}),
]),
},
}),
{ timeout: 5000 },
);

expect(checkBulkUpload).not.toHaveBeenCalledWith({
assetBulkUploadCheckDto: {
Expand Down
8 changes: 6 additions & 2 deletions cli/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineConfig } from 'vite';
import { defineConfig, UserConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
Expand All @@ -17,4 +17,8 @@ export default defineConfig({
noExternal: /^(?!node:).*$/,
},
plugins: [tsconfigPaths()],
});
test: {
name: 'cli:unit',
globals: true,
},
} as UserConfig);
7 changes: 0 additions & 7 deletions cli/vitest.config.ts

This file was deleted.

5 changes: 3 additions & 2 deletions e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"@eslint/js": "^10.0.0",
"@faker-js/faker": "^10.1.0",
"@immich/cli": "workspace:*",
"@immich/e2e-auth-server": "workspace:*",
"@immich/e2e-auth-server": "workspace:*",
"@immich/sdk": "workspace:*",
"@playwright/test": "^1.44.1",
"@socket.io/component-emitter": "^3.1.2",
Expand All @@ -54,7 +54,8 @@
"typescript": "^5.3.3",
"typescript-eslint": "^8.28.0",
"utimes": "^5.2.1",
"vitest": "^3.0.0"
"vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.0.0"
},
"volta": {
"node": "24.13.1"
Expand Down
2 changes: 1 addition & 1 deletion e2e/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@
"esModuleInterop": true,
"baseUrl": "./"
},
"include": ["src/**/*.ts"],
"include": ["src/**/*.ts", "vitest*.config.ts"],
"exclude": ["dist", "node_modules"]
}
10 changes: 5 additions & 5 deletions e2e/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';

const skipDockerSetup = process.env.VITEST_DISABLE_DOCKER_SETUP === 'true';
Expand All @@ -14,15 +15,14 @@ if (!skipDockerSetup) {

export default defineConfig({
test: {
name: 'e2e:server',
retry: process.env.CI ? 4 : 0,
include: ['src/specs/server/**/*.e2e-spec.ts'],
globalSetup,
testTimeout: 15_000,
pool: 'threads',
poolOptions: {
threads: {
singleThread: true,
},
},
maxWorkers: 1,
isolate: false,
},
plugins: [tsconfigPaths()],
});
10 changes: 5 additions & 5 deletions e2e/vitest.maintenance.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';

const skipDockerSetup = process.env.VITEST_DISABLE_DOCKER_SETUP === 'true';
Expand All @@ -14,15 +15,14 @@ if (!skipDockerSetup) {

export default defineConfig({
test: {
name: 'e2e:maintenance',
retry: process.env.CI ? 4 : 0,
include: ['src/specs/maintenance/server/**/*.e2e-spec.ts'],
globalSetup,
testTimeout: 15_000,
pool: 'threads',
poolOptions: {
threads: {
singleThread: true,
},
},
maxWorkers: 1,
isolate: false,
},
plugins: [tsconfigPaths()],
});
17 changes: 4 additions & 13 deletions mobile/lib/domain/models/search_result.model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,21 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';

class SearchResult {
final List<BaseAsset> assets;
final double scrollOffset;
final int? nextPage;

const SearchResult({required this.assets, this.scrollOffset = 0.0, this.nextPage});

SearchResult copyWith({List<BaseAsset>? assets, int? nextPage, double? scrollOffset}) {
return SearchResult(
assets: assets ?? this.assets,
nextPage: nextPage ?? this.nextPage,
scrollOffset: scrollOffset ?? this.scrollOffset,
);
}
const SearchResult({required this.assets, this.nextPage});

@override
String toString() => 'SearchResult(assets: ${assets.length}, nextPage: $nextPage, scrollOffset: $scrollOffset)';
String toString() => 'SearchResult(assets: ${assets.length}, nextPage: $nextPage)';

@override
bool operator ==(covariant SearchResult other) {
if (identical(this, other)) return true;
final listEquals = const DeepCollectionEquality().equals;

return listEquals(other.assets, assets) && other.nextPage == nextPage && other.scrollOffset == scrollOffset;
return listEquals(other.assets, assets) && other.nextPage == nextPage;
}

@override
int get hashCode => assets.hashCode ^ nextPage.hashCode ^ scrollOffset.hashCode;
int get hashCode => assets.hashCode ^ nextPage.hashCode;
}
5 changes: 4 additions & 1 deletion mobile/lib/domain/services/timeline.service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ class TimelineFactory {
TimelineService fromAssets(List<BaseAsset> assets, TimelineOrigin type) =>
TimelineService(_timelineRepository.fromAssets(assets, type));

TimelineService fromAssetStream(List<BaseAsset> Function() getAssets, Stream<int> assetCount, TimelineOrigin type) =>
TimelineService(_timelineRepository.fromAssetStream(getAssets, assetCount, type));

TimelineService fromAssetsWithBuckets(List<BaseAsset> assets, TimelineOrigin type) =>
TimelineService(_timelineRepository.fromAssetsWithBuckets(assets, type));

Expand Down Expand Up @@ -112,7 +115,7 @@ class TimelineService {

if (totalAssets == 0) {
_bufferOffset = 0;
_buffer.clear();
_buffer = [];
} else {
final int offset;
final int count;
Expand Down
Loading
Loading