mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
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:
parent
71fe9192fd
commit
6af534fe4c
65 changed files with 77 additions and 82 deletions
32
.github/workflows/test.yml
vendored
32
.github/workflows/test.yml
vendored
|
|
@ -497,14 +497,15 @@ jobs:
|
||||||
run: npx playwright install chromium --only-shell
|
run: npx playwright install chromium --only-shell
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Docker build
|
- 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() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Run e2e tests (web)
|
- name: Run e2e tests (web)
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
run: npx playwright test --project=chromium
|
PLAYWRIGHT_DISABLE_WEBSERVER: true
|
||||||
|
run: npx playwright test --project=web
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Archive web results
|
- name: Archive e2e test (web) results
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
with:
|
with:
|
||||||
|
|
@ -513,14 +514,37 @@ jobs:
|
||||||
- name: Run ui tests (web)
|
- name: Run ui tests (web)
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
|
PLAYWRIGHT_DISABLE_WEBSERVER: true
|
||||||
run: npx playwright test --project=ui
|
run: npx playwright test --project=ui
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Archive ui results
|
- name: Archive ui test (web) results
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
with:
|
with:
|
||||||
name: e2e-ui-test-results-${{ matrix.runner }}
|
name: e2e-ui-test-results-${{ matrix.runner }}
|
||||||
path: e2e/playwright-report/
|
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:
|
success-check-e2e:
|
||||||
name: End-to-End Tests Success
|
name: End-to-End Tests Success
|
||||||
needs: [e2e-tests-server-cli, e2e-tests-web]
|
needs: [e2e-tests-server-cli, e2e-tests-web]
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ export const playwrightDisableWebserver = process.env.PLAYWRIGHT_DISABLE_WEBSERV
|
||||||
process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS = '1';
|
process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS = '1';
|
||||||
|
|
||||||
const config: PlaywrightTestConfig = {
|
const config: PlaywrightTestConfig = {
|
||||||
testDir: './src/web/specs',
|
testDir: './src/specs/server',
|
||||||
|
testMatch: /.*\.e2e-spec\.ts/,
|
||||||
fullyParallel: false,
|
fullyParallel: false,
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
retries: process.env.CI ? 4 : 0,
|
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),
|
workers: process.env.CI ? 4 : Math.round(cpus().length * 0.75),
|
||||||
|
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'chromium',
|
name: 'web',
|
||||||
use: { ...devices['Desktop Chrome'] },
|
use: { ...devices['Desktop Chrome'] },
|
||||||
testMatch: /.*\.e2e-spec\.ts/,
|
testDir: './src/specs/web',
|
||||||
workers: 1,
|
workers: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ui',
|
name: 'ui',
|
||||||
use: { ...devices['Desktop Chrome'] },
|
use: { ...devices['Desktop Chrome'] },
|
||||||
testMatch: /.*\.ui-spec\.ts/,
|
testDir: './src/ui/specs',
|
||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
workers: process.env.CI ? 3 : Math.max(1, Math.round(cpus().length * 0.75) - 1),
|
workers: process.env.CI ? 3 : Math.max(1, Math.round(cpus().length * 0.75) - 1),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
// {
|
name: 'maintenance',
|
||||||
// name: 'firefox',
|
use: { ...devices['Desktop Chrome'] },
|
||||||
// use: { ...devices['Desktop Firefox'] },
|
testDir: './src/specs/maintenance',
|
||||||
// },
|
workers: 1,
|
||||||
|
},
|
||||||
// {
|
|
||||||
// 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' },
|
|
||||||
// },
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run your local dev server before starting the tests */
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { MemoryType, type MemoryResponseDto, type OnThisDayDto } from '@immich/sdk';
|
import { MemoryType, type MemoryResponseDto, type OnThisDayDto } from '@immich/sdk';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { toAssetResponseDto } from 'src/generators/timeline/rest-response';
|
import { toAssetResponseDto } from 'src/ui/generators/timeline/rest-response';
|
||||||
import type { MockTimelineAsset } from 'src/generators/timeline/timeline-config';
|
import type { MockTimelineAsset } from 'src/ui/generators/timeline/timeline-config';
|
||||||
import { SeededRandom, selectRandomMultiple } from 'src/generators/timeline/utils';
|
import { SeededRandom, selectRandomMultiple } from 'src/ui/generators/timeline/utils';
|
||||||
|
|
||||||
export type MemoryConfig = {
|
export type MemoryConfig = {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { generateConsecutiveDays, generateDayAssets } from 'src/generators/timeline/model-objects';
|
import { generateConsecutiveDays, generateDayAssets } from 'src/ui/generators/timeline/model-objects';
|
||||||
import { SeededRandom, selectRandomDays } from 'src/generators/timeline/utils';
|
import { SeededRandom, selectRandomDays } from 'src/ui/generators/timeline/utils';
|
||||||
import type { MockTimelineAsset } from './timeline-config';
|
import type { MockTimelineAsset } from './timeline-config';
|
||||||
import { GENERATION_CONSTANTS } from './timeline-config';
|
import { GENERATION_CONSTANTS } from './timeline-config';
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import sharp from 'sharp';
|
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) => {
|
export const randomThumbnail = async (seed: string, ratio: number) => {
|
||||||
const height = 235;
|
const height = 235;
|
||||||
|
|
@ -6,7 +6,7 @@ import { faker } from '@faker-js/faker';
|
||||||
import { AssetVisibility } from '@immich/sdk';
|
import { AssetVisibility } from '@immich/sdk';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { writeFileSync } from 'node:fs';
|
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 type { DayPattern, MonthDistribution } from './distribution-patterns';
|
||||||
import { ASSET_DISTRIBUTION, DAY_DISTRIBUTION } from './distribution-patterns';
|
import { ASSET_DISTRIBUTION, DAY_DISTRIBUTION } from './distribution-patterns';
|
||||||
import type { MockTimelineAsset, MockTimelineData, SerializedTimelineData, TimelineConfig } from './timeline-config';
|
import type { MockTimelineAsset, MockTimelineData, SerializedTimelineData, TimelineConfig } from './timeline-config';
|
||||||
|
|
@ -15,7 +15,7 @@ import {
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { signupDto } from 'src/fixtures';
|
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';
|
import type { MockTimelineAsset, MockTimelineData } from './timeline-config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { AssetVisibility } from '@immich/sdk';
|
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
|
// Constants for generation parameters
|
||||||
export const GENERATION_CONSTANTS = {
|
export const GENERATION_CONSTANTS = {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { DateTime } from 'luxon';
|
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
|
* Linear Congruential Generator for deterministic pseudo-random numbers
|
||||||
|
|
@ -10,8 +10,8 @@ import {
|
||||||
randomPreview,
|
randomPreview,
|
||||||
randomThumbnail,
|
randomThumbnail,
|
||||||
TimelineData,
|
TimelineData,
|
||||||
} from 'src/generators/timeline';
|
} from 'src/ui/generators/timeline';
|
||||||
import { sleep } from 'src/web/specs/timeline/utils';
|
import { sleep } from 'src/ui/specs/timeline/utils';
|
||||||
|
|
||||||
export class TimelineTestContext {
|
export class TimelineTestContext {
|
||||||
slowBucket = false;
|
slowBucket = false;
|
||||||
|
|
@ -8,11 +8,11 @@ import {
|
||||||
selectRandom,
|
selectRandom,
|
||||||
TimelineAssetConfig,
|
TimelineAssetConfig,
|
||||||
TimelineData,
|
TimelineData,
|
||||||
} from 'src/generators/timeline';
|
} from 'src/ui/generators/timeline';
|
||||||
import { setupBaseMockApiRoutes } from 'src/mock-network/base-network';
|
import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network';
|
||||||
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network';
|
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network';
|
||||||
import { utils } from 'src/utils';
|
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.configure({ mode: 'parallel' });
|
||||||
test.describe('asset-viewer', () => {
|
test.describe('asset-viewer', () => {
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import type { MemoryResponseDto } from '@immich/sdk';
|
import type { MemoryResponseDto } from '@immich/sdk';
|
||||||
import { test } from '@playwright/test';
|
import { test } from '@playwright/test';
|
||||||
import { generateMemoriesFromTimeline } from 'src/generators/memory';
|
import { generateMemoriesFromTimeline } from 'src/ui/generators/memory';
|
||||||
import {
|
import {
|
||||||
Changes,
|
Changes,
|
||||||
createDefaultTimelineConfig,
|
createDefaultTimelineConfig,
|
||||||
generateTimelineData,
|
generateTimelineData,
|
||||||
TimelineAssetConfig,
|
TimelineAssetConfig,
|
||||||
TimelineData,
|
TimelineData,
|
||||||
} from 'src/generators/timeline';
|
} from 'src/ui/generators/timeline';
|
||||||
import { setupBaseMockApiRoutes } from 'src/mock-network/base-network';
|
import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network';
|
||||||
import { MemoryChanges, setupMemoryMockApiRoutes } from 'src/mock-network/memory-network';
|
import { MemoryChanges, setupMemoryMockApiRoutes } from 'src/ui/mock-network/memory-network';
|
||||||
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network';
|
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network';
|
||||||
import { memoryAssetViewerUtils, memoryGalleryUtils, memoryViewerUtils } from 'src/web/specs/memory/utils';
|
import { memoryAssetViewerUtils, memoryGalleryUtils, memoryViewerUtils } from './utils';
|
||||||
|
|
||||||
test.describe.configure({ mode: 'parallel' });
|
test.describe.configure({ mode: 'parallel' });
|
||||||
|
|
||||||
|
|
@ -6,10 +6,10 @@ import {
|
||||||
generateTimelineData,
|
generateTimelineData,
|
||||||
TimelineAssetConfig,
|
TimelineAssetConfig,
|
||||||
TimelineData,
|
TimelineData,
|
||||||
} from 'src/generators/timeline';
|
} from 'src/ui/generators/timeline';
|
||||||
import { setupBaseMockApiRoutes } from 'src/mock-network/base-network';
|
import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network';
|
||||||
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network';
|
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network';
|
||||||
import { assetViewerUtils } from 'src/web/specs/timeline/utils';
|
import { assetViewerUtils } from '../timeline/utils';
|
||||||
|
|
||||||
const buildSearchUrl = (assetId: string) => {
|
const buildSearchUrl = (assetId: string) => {
|
||||||
const searchQuery = encodeURIComponent(JSON.stringify({ originalFileName: 'test' }));
|
const searchQuery = encodeURIComponent(JSON.stringify({ originalFileName: 'test' }));
|
||||||
|
|
@ -12,18 +12,15 @@ import {
|
||||||
selectRandomMultiple,
|
selectRandomMultiple,
|
||||||
TimelineAssetConfig,
|
TimelineAssetConfig,
|
||||||
TimelineData,
|
TimelineData,
|
||||||
} from 'src/generators/timeline';
|
} from 'src/ui/generators/timeline';
|
||||||
import { setupBaseMockApiRoutes } from 'src/mock-network/base-network';
|
import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network';
|
||||||
import { pageRoutePromise, setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network';
|
|
||||||
import { utils } from 'src/utils';
|
|
||||||
import {
|
import {
|
||||||
assetViewerUtils,
|
pageRoutePromise,
|
||||||
padYearMonth,
|
setupTimelineMockApiRoutes,
|
||||||
pageUtils,
|
TimelineTestContext,
|
||||||
poll,
|
} from 'src/ui/mock-network/timeline-network';
|
||||||
thumbnailUtils,
|
import { utils } from 'src/utils';
|
||||||
timelineUtils,
|
import { assetViewerUtils, padYearMonth, pageUtils, poll, thumbnailUtils, timelineUtils } from './utils';
|
||||||
} from 'src/web/specs/timeline/utils';
|
|
||||||
|
|
||||||
test.describe.configure({ mode: 'parallel' });
|
test.describe.configure({ mode: 'parallel' });
|
||||||
test.describe('Timeline', () => {
|
test.describe('Timeline', () => {
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { BrowserContext, expect, Page } from '@playwright/test';
|
import { BrowserContext, expect, Page } from '@playwright/test';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { TimelineAssetConfig } from 'src/generators/timeline';
|
import { TimelineAssetConfig } from 'src/ui/generators/timeline';
|
||||||
|
|
||||||
export const sleep = (ms: number) => {
|
export const sleep = (ms: number) => {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"rootDirs": ["src"],
|
|
||||||
"baseUrl": "./"
|
"baseUrl": "./"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts"],
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@ import { defineConfig } from 'vitest/config';
|
||||||
// skip `docker compose up` if `make e2e` was already run
|
// skip `docker compose up` if `make e2e` was already run
|
||||||
const globalSetup: string[] = [];
|
const globalSetup: string[] = [];
|
||||||
try {
|
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 {
|
} catch {
|
||||||
globalSetup.push('src/setup/docker-compose.ts');
|
globalSetup.push('src/docker-compose.ts');
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
include: ['src/{api,cli,immich-admin}/specs/*.e2e-spec.ts'],
|
include: ['src/specs/server/**/*.e2e-spec.ts'],
|
||||||
globalSetup,
|
globalSetup,
|
||||||
testTimeout: 15_000,
|
testTimeout: 15_000,
|
||||||
pool: 'threads',
|
pool: 'threads',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue