chore: add web workflow regression tests (#33016)
This commit is contained in:
1 parent
7459863a87
commit
4fdc9b69fa
4 files changed
+158
-1
No files matched your search
@@ -0,0 +1,59 @@
|
||||
import { Route } from '#lib/route.ts';
|
||||
import type { MockData } from '#test/common/data.ts';
|
||||
import { factory } from '#test/unit/factories.ts';
|
||||
import { test } from '#test/unit/fixtures.ts';
|
||||
import type { MockRouter } from '#test/unit/mocks/router.ts';
|
||||
import { describe, expect } from 'vitest';
|
||||
import { page } from 'vitest/browser';
|
||||
import Page from './+page.svelte';
|
||||
import { load } from './+page.ts';
|
||||
|
||||
const favoriteStep = { method: 'immich#favorite', config: null, enabled: true };
|
||||
|
||||
const setup = async (data: MockData, router: MockRouter) => {
|
||||
data.addPluginMethods(factory.pluginMethod({ key: favoriteStep.method, title: 'Mark as favorite' }));
|
||||
const workflow = factory.workflow({ name: 'Favorites', steps: [favoriteStep] });
|
||||
data.addWorkflows(workflow);
|
||||
await router.goto(Route.viewWorkflow(workflow), { workflowId: workflow.id });
|
||||
return workflow;
|
||||
};
|
||||
|
||||
describe('workflow page', () => {
|
||||
test('json view leaves out local step ids', async ({ data, router, render }) => {
|
||||
await setup(data, router);
|
||||
await render(Page, load);
|
||||
|
||||
await page.getByRole('button', { name: 'JSON', exact: true }).click();
|
||||
|
||||
await expect.element(page.getByText(/immich#favorite/).first()).toBeVisible();
|
||||
await expect.element(page.getByText(/step-\d/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('duplicating sends the steps without local ids', async ({ data, router, render, settle, api }) => {
|
||||
await setup(data, router);
|
||||
await render(Page, load);
|
||||
|
||||
await page.getByRole('button', { name: 'duplicate', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'create', exact: true }).click();
|
||||
await settle();
|
||||
|
||||
expect(api.createWorkflow).toHaveBeenCalledWith({
|
||||
body: expect.objectContaining({ steps: [favoriteStep] }),
|
||||
});
|
||||
});
|
||||
|
||||
test('saving a change hides the save button', async ({ data, router, render, settle, api }) => {
|
||||
const workflow = await setup(data, router);
|
||||
await render(Page, load);
|
||||
|
||||
await page.getByPlaceholder('workflow_name').fill('Renamed');
|
||||
await page.getByRole('button', { name: 'save', exact: true }).click();
|
||||
await settle();
|
||||
|
||||
expect(api.updateWorkflow).toHaveBeenCalledWith({
|
||||
params: { id: workflow.id },
|
||||
body: expect.objectContaining({ name: 'Renamed' }),
|
||||
});
|
||||
await expect.element(page.getByRole('button', { name: 'save', exact: true })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type MemoryUpdateDto,
|
||||
type MetadataSearchDto,
|
||||
type PersonResponseDto,
|
||||
type PluginMethodResponseDto,
|
||||
type SearchResponseDto,
|
||||
type SharedLinkResponseDto,
|
||||
type StackResponseDto,
|
||||
@@ -33,6 +34,10 @@ import {
|
||||
type UserAdminResponseDto,
|
||||
type UserAdminUpdateDto,
|
||||
type UserPreferencesResponseDto,
|
||||
type WorkflowCreateDto,
|
||||
type WorkflowResponseDto,
|
||||
type WorkflowStepDto,
|
||||
type WorkflowUpdateDto,
|
||||
} from '@immich/sdk';
|
||||
|
||||
export type TimelineFilter = {
|
||||
@@ -57,6 +62,9 @@ const monthKey = (iso: string) => {
|
||||
type MaybeArray<T> = T | T[];
|
||||
const toList = <T>(items: MaybeArray<T>) => (Array.isArray(items) ? items : [items]);
|
||||
|
||||
const toWorkflowSteps = (steps: WorkflowStepDto[]) =>
|
||||
steps.map(({ method, config, enabled }) => ({ method, config, enabled }));
|
||||
|
||||
const hiddenVisibilities = new Set<AssetVisibility>([AssetVisibility.Hidden, AssetVisibility.Locked]);
|
||||
|
||||
export class MockData {
|
||||
@@ -89,6 +97,8 @@ export class MockData {
|
||||
private readonly sharedLinks = new Map<string, SharedLinkResponseDto>();
|
||||
private readonly tags = new Map<string, TagResponseDto>();
|
||||
private readonly activities = new Map<string, ActivityResponseDto>();
|
||||
private readonly workflows = new Map<string, WorkflowResponseDto>();
|
||||
private readonly pluginMethods = new Map<string, PluginMethodResponseDto>();
|
||||
|
||||
getPreferences() {
|
||||
return this.preferences;
|
||||
@@ -403,6 +413,45 @@ export class MockData {
|
||||
return this.activities.delete(id);
|
||||
}
|
||||
|
||||
addWorkflows(workflows: MaybeArray<WorkflowResponseDto>) {
|
||||
for (const workflow of toList(workflows)) {
|
||||
this.workflows.set(workflow.id, workflow);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
workflowList() {
|
||||
return [...this.workflows.values()];
|
||||
}
|
||||
|
||||
createWorkflow(dto: WorkflowCreateDto) {
|
||||
const workflow = factory.workflow({ ...dto, steps: toWorkflowSteps(dto.steps ?? []) });
|
||||
this.workflows.set(workflow.id, workflow);
|
||||
return workflow;
|
||||
}
|
||||
|
||||
updateWorkflow(id: string, dto: WorkflowUpdateDto) {
|
||||
const workflow = this.workflows.get(id);
|
||||
if (!workflow) {
|
||||
return;
|
||||
}
|
||||
const steps = dto.steps ? toWorkflowSteps(dto.steps) : workflow.steps;
|
||||
const updated = { ...workflow, ...dto, steps, updatedAt: new Date().toISOString() };
|
||||
this.workflows.set(id, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
addPluginMethods(methods: MaybeArray<PluginMethodResponseDto>) {
|
||||
for (const method of toList(methods)) {
|
||||
this.pluginMethods.set(method.key, method);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
pluginMethodList() {
|
||||
return [...this.pluginMethods.values()];
|
||||
}
|
||||
|
||||
updateAdminConfig(config: AdminConfigDto) {
|
||||
this.adminConfig = config;
|
||||
return config;
|
||||
|
||||
@@ -10,17 +10,21 @@ import {
|
||||
SharedLinkType,
|
||||
UserAvatarColor,
|
||||
UserStatus,
|
||||
WorkflowTrigger,
|
||||
WorkflowType,
|
||||
type ActivityResponseDto,
|
||||
type AlbumResponseDto,
|
||||
type AssetOcrResponseDto,
|
||||
type AssetResponseDto,
|
||||
type MemoryResponseDto,
|
||||
type PersonResponseDto,
|
||||
type PluginMethodResponseDto,
|
||||
type SharedLinkResponseDto,
|
||||
type StackResponseDto,
|
||||
type TagResponseDto,
|
||||
type UserAdminResponseDto,
|
||||
type UserPreferencesResponseDto,
|
||||
type WorkflowResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
@@ -29,6 +33,7 @@ export const adminUserId = '0d9a7a2e-0000-4000-8000-00000000ad01';
|
||||
let assetIndex = 0;
|
||||
let personIndex = 0;
|
||||
let tagIndex = 0;
|
||||
let pluginMethodIndex = 0;
|
||||
const random = new SeededRandom(7);
|
||||
|
||||
const names = ['Alice Johnson', 'Bob Smith', 'Charlie Brown', 'Diana Prince', 'Eve Adams', 'Frank Castle'];
|
||||
@@ -270,6 +275,36 @@ const tag = (overrides: Partial<TagResponseDto> = {}): TagResponseDto => {
|
||||
};
|
||||
};
|
||||
|
||||
const workflow = (overrides: Partial<WorkflowResponseDto> = {}): WorkflowResponseDto => {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
name: 'Workflow',
|
||||
description: null,
|
||||
enabled: true,
|
||||
logging: false,
|
||||
trigger: WorkflowTrigger.AssetCreate,
|
||||
steps: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
const pluginMethod = (overrides: Partial<PluginMethodResponseDto> = {}): PluginMethodResponseDto => {
|
||||
const key = overrides.key ?? `plugin#method-${pluginMethodIndex++}`;
|
||||
return {
|
||||
key,
|
||||
name: key,
|
||||
title: key,
|
||||
description: '',
|
||||
hostFunctions: false,
|
||||
types: [WorkflowType.AssetV1],
|
||||
uiHints: [],
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
const sharedLink = (overrides: Partial<SharedLinkResponseDto> = {}): SharedLinkResponseDto => ({
|
||||
id: crypto.randomUUID(),
|
||||
key: alphanumeric(64),
|
||||
@@ -518,6 +553,8 @@ export const factory = {
|
||||
ocr,
|
||||
tag,
|
||||
sharedLink,
|
||||
workflow,
|
||||
pluginMethod,
|
||||
memories,
|
||||
timeline,
|
||||
many,
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { MockData } from '#test/common/data.ts';
|
||||
import { factory } from '#test/common/factories.ts';
|
||||
import { fromBase64, minimalMp4, placeholderImage } from '#test/common/helpers.ts';
|
||||
import type { Handlers } from '#test/common/types.ts';
|
||||
import { AssetUploadAction } from '@immich/sdk';
|
||||
import { AssetUploadAction, WorkflowTrigger, WorkflowType } from '@immich/sdk';
|
||||
|
||||
const notFound = { status: 404, json: { message: 'Not found', statusCode: 404 } };
|
||||
const passwordRequired = { status: 401, json: { message: 'Password required', statusCode: 401 } };
|
||||
@@ -174,6 +174,18 @@ export const createHandlers = (data: MockData): Handlers => {
|
||||
headers: { 'Content-Disposition': 'attachment; filename="immich.zip"' },
|
||||
}),
|
||||
|
||||
searchWorkflows: () => ({ json: data.workflowList() }),
|
||||
getWorkflowTriggers: () => ({
|
||||
json: Object.values(WorkflowTrigger).map((trigger) => ({ trigger, types: [WorkflowType.AssetV1] })),
|
||||
}),
|
||||
createWorkflow: ({ body }) => ({ status: 201, json: data.createWorkflow(body) }),
|
||||
updateWorkflow: ({ params, body }) => {
|
||||
const workflow = data.updateWorkflow(params.id, body);
|
||||
return workflow ? { json: workflow } : notFound;
|
||||
},
|
||||
searchPluginMethods: () => ({ json: data.pluginMethodList() }),
|
||||
searchPluginTemplates: () => ({ json: [] }),
|
||||
|
||||
playAssetVideo: () => ({ body: minimalMp4, contentType: 'video/mp4' }),
|
||||
viewAsset: ({ params }) => media(params.id),
|
||||
downloadAsset: ({ params }) => media(params.id),
|
||||
|
||||
Reference in new issue
Block a user