feat: run maintenance tests in isolation, share containers between all … (#25856)

* feat: run maintance tests in isolation, share containers between all serial test suites

* refactor: organize files

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
Min Idzelis
2026-02-10 11:05:06 -05:00
committed by GitHub
parent 71fe9192fd
commit 6af534fe4c
65 changed files with 77 additions and 82 deletions

View File

@@ -497,14 +497,15 @@ jobs:
run: npx playwright install chromium --only-shell
if: ${{ !cancelled() }}
- name: Docker build
run: docker compose build
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
if: ${{ !cancelled() }}
- name: Run e2e tests (web)
env:
CI: true
run: npx playwright test --project=chromium
PLAYWRIGHT_DISABLE_WEBSERVER: true
run: npx playwright test --project=web
if: ${{ !cancelled() }}
- name: Archive web results
- name: Archive e2e test (web) results
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: success() || failure()
with:
@@ -513,14 +514,37 @@ jobs:
- name: Run ui tests (web)
env:
CI: true
PLAYWRIGHT_DISABLE_WEBSERVER: true
run: npx playwright test --project=ui
if: ${{ !cancelled() }}
- name: Archive ui results
- name: Archive ui test (web) results
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: success() || failure()
with:
name: e2e-ui-test-results-${{ matrix.runner }}
path: e2e/playwright-report/
- name: Run maintenance tests
env:
CI: true
PLAYWRIGHT_DISABLE_WEBSERVER: true
run: npx playwright test --project=maintenance
if: ${{ !cancelled() }}
- name: Archive maintenance tests (web) results
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: success() || failure()
with:
name: e2e-maintenance-isolated-test-results-${{ matrix.runner }}
path: e2e/playwright-report/
- name: Capture Docker logs
if: always()
run: docker compose logs --no-color > docker-compose-logs.txt
working-directory: ./e2e
- name: Archive Docker logs
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: always()
with:
name: docker-compose-logs-${{ matrix.runner }}
path: e2e/docker-compose-logs.txt
success-check-e2e:
name: End-to-End Tests Success
needs: [e2e-tests-server-cli, e2e-tests-web]

View File

@@ -14,7 +14,8 @@ export const playwrightDisableWebserver = process.env.PLAYWRIGHT_DISABLE_WEBSERV
process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS = '1';
const config: PlaywrightTestConfig = {
testDir: './src/web/specs',
testDir: './src/specs/server',
testMatch: /.*\.e2e-spec\.ts/,
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 4 : 0,
@@ -28,54 +29,28 @@ const config: PlaywrightTestConfig = {
},
},
testMatch: /.*\.e2e-spec\.ts/,
workers: process.env.CI ? 4 : Math.round(cpus().length * 0.75),
projects: [
{
name: 'chromium',
name: 'web',
use: { ...devices['Desktop Chrome'] },
testMatch: /.*\.e2e-spec\.ts/,
testDir: './src/specs/web',
workers: 1,
},
{
name: 'ui',
use: { ...devices['Desktop Chrome'] },
testMatch: /.*\.ui-spec\.ts/,
testDir: './src/ui/specs',
fullyParallel: true,
workers: process.env.CI ? 3 : Math.max(1, Math.round(cpus().length * 0.75) - 1),
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
{
name: 'maintenance',
use: { ...devices['Desktop Chrome'] },
testDir: './src/specs/maintenance',
workers: 1,
},
],
/* Run your local dev server before starting the tests */

View File

@@ -1,9 +1,9 @@
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';
import { toAssetResponseDto } from 'src/ui/generators/timeline/rest-response';
import type { MockTimelineAsset } from 'src/ui/generators/timeline/timeline-config';
import { SeededRandom, selectRandomMultiple } from 'src/ui/generators/timeline/utils';
export type MemoryConfig = {
id?: string;

View File

@@ -1,5 +1,5 @@
import { generateConsecutiveDays, generateDayAssets } from 'src/generators/timeline/model-objects';
import { SeededRandom, selectRandomDays } from 'src/generators/timeline/utils';
import { generateConsecutiveDays, generateDayAssets } from 'src/ui/generators/timeline/model-objects';
import { SeededRandom, selectRandomDays } from 'src/ui/generators/timeline/utils';
import type { MockTimelineAsset } from './timeline-config';
import { GENERATION_CONSTANTS } from './timeline-config';

View File

@@ -1,5 +1,5 @@
import sharp from 'sharp';
import { SeededRandom } from 'src/generators/timeline/utils';
import { SeededRandom } from 'src/ui/generators/timeline/utils';
export const randomThumbnail = async (seed: string, ratio: number) => {
const height = 235;

View File

@@ -6,7 +6,7 @@ import { faker } from '@faker-js/faker';
import { AssetVisibility } from '@immich/sdk';
import { DateTime } from 'luxon';
import { writeFileSync } from 'node:fs';
import { SeededRandom } from 'src/generators/timeline/utils';
import { SeededRandom } from 'src/ui/generators/timeline/utils';
import type { DayPattern, MonthDistribution } from './distribution-patterns';
import { ASSET_DISTRIBUTION, DAY_DISTRIBUTION } from './distribution-patterns';
import type { MockTimelineAsset, MockTimelineData, SerializedTimelineData, TimelineConfig } from './timeline-config';

View File

@@ -15,7 +15,7 @@ import {
} from '@immich/sdk';
import { DateTime } from 'luxon';
import { signupDto } from 'src/fixtures';
import { parseTimeBucketKey } from 'src/generators/timeline/utils';
import { parseTimeBucketKey } from 'src/ui/generators/timeline/utils';
import type { MockTimelineAsset, MockTimelineData } from './timeline-config';
/**

View File

@@ -1,5 +1,5 @@
import type { AssetVisibility } from '@immich/sdk';
import { DayPattern, MonthDistribution } from 'src/generators/timeline/distribution-patterns';
import { DayPattern, MonthDistribution } from 'src/ui/generators/timeline/distribution-patterns';
// Constants for generation parameters
export const GENERATION_CONSTANTS = {

View File

@@ -1,5 +1,5 @@
import { DateTime } from 'luxon';
import { GENERATION_CONSTANTS, MockTimelineAsset } from 'src/generators/timeline/timeline-config';
import { GENERATION_CONSTANTS, MockTimelineAsset } from 'src/ui/generators/timeline/timeline-config';
/**
* Linear Congruential Generator for deterministic pseudo-random numbers

View File

@@ -10,8 +10,8 @@ import {
randomPreview,
randomThumbnail,
TimelineData,
} from 'src/generators/timeline';
import { sleep } from 'src/web/specs/timeline/utils';
} from 'src/ui/generators/timeline';
import { sleep } from 'src/ui/specs/timeline/utils';
export class TimelineTestContext {
slowBucket = false;

View File

@@ -8,11 +8,11 @@ import {
selectRandom,
TimelineAssetConfig,
TimelineData,
} from 'src/generators/timeline';
import { setupBaseMockApiRoutes } from 'src/mock-network/base-network';
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network';
} from 'src/ui/generators/timeline';
import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network';
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network';
import { utils } from 'src/utils';
import { assetViewerUtils } from 'src/web/specs/timeline/utils';
import { assetViewerUtils } from '../timeline/utils';
test.describe.configure({ mode: 'parallel' });
test.describe('asset-viewer', () => {

View File

@@ -1,18 +1,18 @@
import { faker } from '@faker-js/faker';
import type { MemoryResponseDto } from '@immich/sdk';
import { test } from '@playwright/test';
import { generateMemoriesFromTimeline } from 'src/generators/memory';
import { generateMemoriesFromTimeline } from 'src/ui/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';
} from 'src/ui/generators/timeline';
import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network';
import { MemoryChanges, setupMemoryMockApiRoutes } from 'src/ui/mock-network/memory-network';
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network';
import { memoryAssetViewerUtils, memoryGalleryUtils, memoryViewerUtils } from './utils';
test.describe.configure({ mode: 'parallel' });

View File

@@ -6,10 +6,10 @@ import {
generateTimelineData,
TimelineAssetConfig,
TimelineData,
} from 'src/generators/timeline';
import { setupBaseMockApiRoutes } from 'src/mock-network/base-network';
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network';
import { assetViewerUtils } from 'src/web/specs/timeline/utils';
} from 'src/ui/generators/timeline';
import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network';
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network';
import { assetViewerUtils } from '../timeline/utils';
const buildSearchUrl = (assetId: string) => {
const searchQuery = encodeURIComponent(JSON.stringify({ originalFileName: 'test' }));

View File

@@ -12,18 +12,15 @@ import {
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';
} from 'src/ui/generators/timeline';
import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network';
import {
assetViewerUtils,
padYearMonth,
pageUtils,
poll,
thumbnailUtils,
timelineUtils,
} from 'src/web/specs/timeline/utils';
pageRoutePromise,
setupTimelineMockApiRoutes,
TimelineTestContext,
} from 'src/ui/mock-network/timeline-network';
import { utils } from 'src/utils';
import { assetViewerUtils, padYearMonth, pageUtils, poll, thumbnailUtils, timelineUtils } from './utils';
test.describe.configure({ mode: 'parallel' });
test.describe('Timeline', () => {

View File

@@ -1,6 +1,6 @@
import { BrowserContext, expect, Page } from '@playwright/test';
import { DateTime } from 'luxon';
import { TimelineAssetConfig } from 'src/generators/timeline';
import { TimelineAssetConfig } from 'src/ui/generators/timeline';
export const sleep = (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms));

View File

@@ -15,7 +15,6 @@
"incremental": true,
"skipLibCheck": true,
"esModuleInterop": true,
"rootDirs": ["src"],
"baseUrl": "./"
},
"include": ["src/**/*.ts"],

View File

@@ -3,14 +3,14 @@ import { defineConfig } from 'vitest/config';
// skip `docker compose up` if `make e2e` was already run
const globalSetup: string[] = [];
try {
await fetch('http://127.0.0.1:2285/api/server-info/ping');
await fetch('http://127.0.0.1:2285/api/server/ping');
} catch {
globalSetup.push('src/setup/docker-compose.ts');
globalSetup.push('src/docker-compose.ts');
}
export default defineConfig({
test: {
include: ['src/{api,cli,immich-admin}/specs/*.e2e-spec.ts'],
include: ['src/specs/server/**/*.e2e-spec.ts'],
globalSetup,
testTimeout: 15_000,
pool: 'threads',