chore: add tests for confirm flows (#33054)
(cherry picked from commit 71e37b154e359328a61d1952c0da046cc5cc93c2)
This commit is contained in:
1 parent
48cfafd1c1
commit
55df2bb2bf
15 files changed
+641
-4
No files matched your search
+44
@@ -142,3 +142,47 @@ test('unliking an album removes only my album like', async ({ data, router, rend
|
||||
expect(api.deleteActivity).toHaveBeenCalledWith({ params: { id: myLike.id } });
|
||||
expect(api.deleteActivity.calls).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('deletes an album', async ({ data, router, render, settle, api, app }) => {
|
||||
const album = factory.album({ albumUsers: factory.albumUsers({ owner: data.user }) });
|
||||
data.addAlbums(album);
|
||||
await router.goto(Route.viewAlbum(album), { albumId: album.id });
|
||||
await render(Page, load);
|
||||
await settle();
|
||||
|
||||
await app.album.optionsMenuButton.click();
|
||||
await app.album.deleteMenuItem.click();
|
||||
await app.cancelButton.click();
|
||||
await settle();
|
||||
|
||||
expect(api.deleteAlbum).not.toHaveBeenCalled();
|
||||
|
||||
await app.album.optionsMenuButton.click();
|
||||
await app.album.deleteMenuItem.click();
|
||||
await app.confirmButton.click();
|
||||
await settle();
|
||||
|
||||
expect(api.deleteAlbum).toHaveBeenCalledWith({ params: { id: album.id } });
|
||||
});
|
||||
|
||||
test('leaves an album', async ({ data, router, render, settle, api, app }) => {
|
||||
const album = factory.album({ albumUsers: factory.albumUsers({ owner: factory.user(), viewers: [data.user] }) });
|
||||
data.addAlbums(album);
|
||||
await router.goto(Route.viewAlbum(album), { albumId: album.id });
|
||||
await render(Page, load);
|
||||
await settle();
|
||||
|
||||
await app.album.optionsMenuButton.click();
|
||||
await app.album.leaveMenuItem.click();
|
||||
await app.cancelButton.click();
|
||||
await settle();
|
||||
|
||||
expect(api.removeUserFromAlbum).not.toHaveBeenCalled();
|
||||
|
||||
await app.album.optionsMenuButton.click();
|
||||
await app.album.leaveMenuItem.click();
|
||||
await app.dialogButton('leave').click();
|
||||
await settle();
|
||||
|
||||
expect(api.removeUserFromAlbum).toHaveBeenCalledWith({ params: { id: album.id, userId: 'me' } });
|
||||
});
|
||||
@@ -45,6 +45,12 @@ test('deleting the current tag opens its parent', async ({ data, router, render,
|
||||
await render(Page, load);
|
||||
await settle();
|
||||
|
||||
await app.tags.deleteButton.click();
|
||||
await app.cancelButton.click();
|
||||
await settle();
|
||||
|
||||
expect(api.deleteTag).not.toHaveBeenCalled();
|
||||
|
||||
await app.tags.deleteButton.click();
|
||||
await app.tags.form.deleteButton.click();
|
||||
await settle();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { OpenQueryParam } from '#lib/constants.ts';
|
||||
import { Route } from '#lib/route.ts';
|
||||
import { factory } from '#test/unit/factories.ts';
|
||||
import { test } from '#test/unit/fixtures.ts';
|
||||
import { expect } from 'vitest';
|
||||
import Page from './+page.svelte';
|
||||
@@ -15,3 +16,103 @@ test('links to the OAuth account management page', async ({ data, router, render
|
||||
|
||||
await expect.element(app.settings.manageOauthAccountLink).toHaveAttribute('href', 'https://idp.example/account');
|
||||
});
|
||||
|
||||
test('rotates an api key', async ({ data, router, render, settle, api, app }) => {
|
||||
const apiKey = factory.apiKey({ name: 'CLI' });
|
||||
data.addApiKeys(apiKey);
|
||||
await router.goto(Route.userSettings());
|
||||
await render(Page, load);
|
||||
await app.settings.section('api_keys').click();
|
||||
|
||||
await app.settings.rotateApiKeyButton.click();
|
||||
await app.cancelButton.click();
|
||||
await settle();
|
||||
|
||||
expect(api.rotateApiKey).not.toHaveBeenCalled();
|
||||
|
||||
await app.settings.rotateApiKeyButton.click();
|
||||
await app.confirmButton.click();
|
||||
await settle();
|
||||
|
||||
expect(api.rotateApiKey).toHaveBeenCalledWith({ params: { id: apiKey.id } });
|
||||
await app.settings.apiKeySecretDoneButton.click();
|
||||
});
|
||||
|
||||
test('deletes an api key', async ({ data, router, render, settle, api, app }) => {
|
||||
const apiKey = factory.apiKey({ name: 'CLI' });
|
||||
data.addApiKeys(apiKey);
|
||||
await router.goto(Route.userSettings());
|
||||
await render(Page, load);
|
||||
await app.settings.section('api_keys').click();
|
||||
|
||||
await app.settings.deleteApiKeyButton.click();
|
||||
await app.cancelButton.click();
|
||||
await settle();
|
||||
|
||||
expect(api.deleteApiKey).not.toHaveBeenCalled();
|
||||
|
||||
await app.settings.deleteApiKeyButton.click();
|
||||
await app.confirmButton.click();
|
||||
await settle();
|
||||
|
||||
expect(api.deleteApiKey).toHaveBeenCalledWith({ params: { id: apiKey.id } });
|
||||
});
|
||||
|
||||
test('deletes a passkey', async ({ data, router, render, settle, api, app }) => {
|
||||
const passkey = factory.passkey({ name: 'Laptop' });
|
||||
data.addPasskeys(passkey);
|
||||
await router.goto(Route.userSettings());
|
||||
await render(Page, load);
|
||||
await app.settings.section('passkeys').click();
|
||||
|
||||
await app.settings.deletePasskeyButton.click();
|
||||
await app.cancelButton.click();
|
||||
await settle();
|
||||
|
||||
expect(api.deletePasskey).not.toHaveBeenCalled();
|
||||
|
||||
await app.settings.deletePasskeyButton.click();
|
||||
await app.confirmButton.click();
|
||||
await settle();
|
||||
|
||||
expect(api.deletePasskey).toHaveBeenCalledWith({ params: { id: passkey.id } });
|
||||
});
|
||||
|
||||
test('logs out a device', async ({ data, router, render, settle, api, app }) => {
|
||||
const other = factory.session({ deviceOS: 'iOS' });
|
||||
data.addSessions([factory.session({ current: true }), other]);
|
||||
await router.goto(Route.userSettings());
|
||||
await render(Page, load);
|
||||
await app.settings.section('authorized_devices').click();
|
||||
|
||||
await app.settings.logOutDeviceButton.click();
|
||||
await app.cancelButton.click();
|
||||
await settle();
|
||||
|
||||
expect(api.deleteSession).not.toHaveBeenCalled();
|
||||
|
||||
await app.settings.logOutDeviceButton.click();
|
||||
await app.confirmButton.click();
|
||||
await settle();
|
||||
|
||||
expect(api.deleteSession).toHaveBeenCalledWith({ params: { id: other.id } });
|
||||
});
|
||||
|
||||
test('logs out all devices', async ({ data, router, render, settle, api, app }) => {
|
||||
data.addSessions([factory.session({ current: true }), factory.session()]);
|
||||
await router.goto(Route.userSettings());
|
||||
await render(Page, load);
|
||||
await app.settings.section('authorized_devices').click();
|
||||
|
||||
await app.settings.logOutAllDevicesButton.click();
|
||||
await app.cancelButton.click();
|
||||
await settle();
|
||||
|
||||
expect(api.deleteAllSessions).not.toHaveBeenCalled();
|
||||
|
||||
await app.settings.logOutAllDevicesButton.click();
|
||||
await app.confirmButton.click();
|
||||
await settle();
|
||||
|
||||
expect(api.deleteAllSessions).toHaveBeenCalled();
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Route } from '#lib/route.ts';
|
||||
import { factory } from '#test/unit/factories.ts';
|
||||
import { test } from '#test/unit/fixtures.ts';
|
||||
import { expect } from 'vitest';
|
||||
import Page from './+page.svelte';
|
||||
import { load } from './+page.ts';
|
||||
|
||||
test('deletes a workflow', async ({ data, router, render, settle, api, app }) => {
|
||||
const workflow = factory.workflow({ name: 'Favorites' });
|
||||
data.addWorkflows(workflow);
|
||||
await router.goto(Route.workflows());
|
||||
await render(Page, load);
|
||||
await settle();
|
||||
|
||||
await app.workflows.menuButton(0).click();
|
||||
await app.workflows.deleteMenuItem.click();
|
||||
await app.cancelButton.click();
|
||||
await settle();
|
||||
|
||||
expect(api.deleteWorkflow).not.toHaveBeenCalled();
|
||||
|
||||
await app.workflows.menuButton(0).click();
|
||||
await app.workflows.deleteMenuItem.click();
|
||||
await app.confirmButton.click();
|
||||
await settle();
|
||||
|
||||
expect(api.deleteWorkflow).toHaveBeenCalledWith({ params: { id: workflow.id } });
|
||||
});
|
||||
@@ -14,7 +14,9 @@ import {
|
||||
type ActivityResponseDto,
|
||||
type AdminConfigDto,
|
||||
type AlbumResponseDto,
|
||||
type ApiKeyResponseDto,
|
||||
type AssetBulkUpdateDto,
|
||||
type AssetFaceResponseDto,
|
||||
type AssetOcrResponseDto,
|
||||
type AssetResponseDto,
|
||||
type CreateAlbumDto,
|
||||
@@ -22,10 +24,12 @@ import {
|
||||
type DuplicateResolveDto,
|
||||
type DuplicateResponseDto,
|
||||
type ExifResponseDto,
|
||||
type LibraryResponseDto,
|
||||
type MapMarkerResponseDto,
|
||||
type MemoryResponseDto,
|
||||
type MemoryUpdateDto,
|
||||
type MetadataSearchDto,
|
||||
type PasskeyResponseDto,
|
||||
type PersonResponseDto,
|
||||
type PersonUpdateDto,
|
||||
type PlacesResponseDto,
|
||||
@@ -37,6 +41,7 @@ import {
|
||||
type QueueUpdateDto,
|
||||
type SearchExploreResponseDto,
|
||||
type SearchResponseDto,
|
||||
type SessionResponseDto,
|
||||
type SharedLinkResponseDto,
|
||||
type StackCreateDto,
|
||||
type StackResponseDto,
|
||||
@@ -47,6 +52,7 @@ import {
|
||||
type TimeBucketsResponseDto,
|
||||
type UpdateAlbumDto,
|
||||
type UpdateAssetDto,
|
||||
type UpdateLibraryDto,
|
||||
type UserAdminCreateDto,
|
||||
type UserAdminResponseDto,
|
||||
type UserAdminUpdateDto,
|
||||
@@ -144,6 +150,11 @@ export class MockData {
|
||||
private readonly tags = new Map<string, TagResponseDto>();
|
||||
private readonly activities = new Map<string, ActivityResponseDto>();
|
||||
private readonly workflows = new Map<string, WorkflowResponseDto>();
|
||||
private readonly apiKeys = new Map<string, ApiKeyResponseDto>();
|
||||
private readonly libraries = new Map<string, LibraryResponseDto>();
|
||||
private readonly faces = new Map<string, AssetFaceResponseDto[]>();
|
||||
private readonly sessions = new Map<string, SessionResponseDto>();
|
||||
private readonly passkeys = new Map<string, PasskeyResponseDto>();
|
||||
private readonly pluginMethods = new Map<string, PluginMethodResponseDto>();
|
||||
private readonly queueStates = new Map<QueueName, QueueResponseDto>();
|
||||
|
||||
@@ -325,6 +336,10 @@ export class MockData {
|
||||
}
|
||||
}
|
||||
|
||||
removeSharedLink(id: string) {
|
||||
return this.sharedLinks.delete(id);
|
||||
}
|
||||
|
||||
removeSharedLinkAssets(id: string, assetIds: string[]) {
|
||||
const link = this.sharedLinks.get(id);
|
||||
if (!link) {
|
||||
@@ -430,6 +445,17 @@ export class MockData {
|
||||
return this;
|
||||
}
|
||||
|
||||
removeUserFromAlbum(albumId: string, userId: string) {
|
||||
const album = this.albums.get(albumId);
|
||||
if (!album) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const id = userId === 'me' ? this.user.id : userId;
|
||||
this.albums.set(albumId, { ...album, albumUsers: album.albumUsers.filter(({ user }) => user.id !== id) });
|
||||
return true;
|
||||
}
|
||||
|
||||
addDuplicates(assets: AssetResponseDto[]) {
|
||||
const duplicateId = crypto.randomUUID();
|
||||
for (const asset of assets) {
|
||||
@@ -586,6 +612,122 @@ export class MockData {
|
||||
return tag;
|
||||
}
|
||||
|
||||
addFaces(assetId: string, faces: MaybeArray<AssetFaceResponseDto>) {
|
||||
this.faces.set(assetId, [...(this.faces.get(assetId) ?? []), ...toList(faces)]);
|
||||
return this;
|
||||
}
|
||||
|
||||
getFaces(assetId: string) {
|
||||
return this.faces.get(assetId) ?? [];
|
||||
}
|
||||
|
||||
deleteFace(id: string) {
|
||||
for (const [assetId, faces] of this.faces) {
|
||||
if (faces.some((face) => face.id === id)) {
|
||||
this.faces.set(
|
||||
assetId,
|
||||
faces.filter((face) => face.id !== id),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
addLibraries(libraries: MaybeArray<LibraryResponseDto>) {
|
||||
for (const library of toList(libraries)) {
|
||||
this.libraries.set(library.id, library);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
libraryList() {
|
||||
return [...this.libraries.values()];
|
||||
}
|
||||
|
||||
getLibrary(id: string) {
|
||||
return this.libraries.get(id);
|
||||
}
|
||||
|
||||
updateLibrary(id: string, dto: UpdateLibraryDto) {
|
||||
const library = this.libraries.get(id);
|
||||
if (!library) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = { ...library, ...dto, updatedAt: new Date().toISOString() };
|
||||
this.libraries.set(id, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
deleteLibrary(id: string) {
|
||||
return this.libraries.delete(id);
|
||||
}
|
||||
|
||||
addApiKeys(apiKeys: MaybeArray<ApiKeyResponseDto>) {
|
||||
for (const apiKey of toList(apiKeys)) {
|
||||
this.apiKeys.set(apiKey.id, apiKey);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
apiKeyList() {
|
||||
return [...this.apiKeys.values()];
|
||||
}
|
||||
|
||||
rotateApiKey(id: string) {
|
||||
const apiKey = this.apiKeys.get(id);
|
||||
if (!apiKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rotated = { ...apiKey, updatedAt: new Date().toISOString() };
|
||||
this.apiKeys.set(id, rotated);
|
||||
return { ...rotated, apiKey: rotated, secret: 'rotated-secret' };
|
||||
}
|
||||
|
||||
deleteApiKey(id: string) {
|
||||
return this.apiKeys.delete(id);
|
||||
}
|
||||
|
||||
addSessions(sessions: MaybeArray<SessionResponseDto>) {
|
||||
for (const session of toList(sessions)) {
|
||||
this.sessions.set(session.id, session);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
sessionList() {
|
||||
return [...this.sessions.values()];
|
||||
}
|
||||
|
||||
deleteSession(id: string) {
|
||||
return this.sessions.delete(id);
|
||||
}
|
||||
|
||||
deleteOtherSessions() {
|
||||
for (const session of this.sessionList()) {
|
||||
if (!session.current) {
|
||||
this.sessions.delete(session.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addPasskeys(passkeys: MaybeArray<PasskeyResponseDto>) {
|
||||
for (const passkey of toList(passkeys)) {
|
||||
this.passkeys.set(passkey.id, passkey);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
passkeyList() {
|
||||
return [...this.passkeys.values()];
|
||||
}
|
||||
|
||||
deletePasskey(id: string) {
|
||||
return this.passkeys.delete(id);
|
||||
}
|
||||
|
||||
addActivities(activities: MaybeArray<ActivityResponseDto>) {
|
||||
for (const activity of toList(activities)) {
|
||||
this.activities.set(activity.id, activity);
|
||||
@@ -634,6 +776,10 @@ export class MockData {
|
||||
return [...this.workflows.values()];
|
||||
}
|
||||
|
||||
deleteWorkflow(id: string) {
|
||||
return this.workflows.delete(id);
|
||||
}
|
||||
|
||||
createWorkflow(dto: WorkflowCreateDto) {
|
||||
const workflow = factory.workflow({ ...dto, steps: toWorkflowSteps(dto.steps ?? []) });
|
||||
this.workflows.set(workflow.id, workflow);
|
||||
|
||||
@@ -15,13 +15,18 @@ import {
|
||||
type ActivityResponseDto,
|
||||
type AlbumResponseDto,
|
||||
type AlbumUserResponseDto,
|
||||
type ApiKeyResponseDto,
|
||||
type AssetFaceResponseDto,
|
||||
type AssetOcrResponseDto,
|
||||
type AssetResponseDto,
|
||||
type LibraryResponseDto,
|
||||
type MemoryResponseDto,
|
||||
type PasskeyResponseDto,
|
||||
type PersonResponseDto,
|
||||
type PluginMethodResponseDto,
|
||||
type QueueResponseDto,
|
||||
type QueueStatisticsDto,
|
||||
type SessionResponseDto,
|
||||
type SharedLinkResponseDto,
|
||||
type StackResponseDto,
|
||||
type TagResponseDto,
|
||||
@@ -290,6 +295,59 @@ const tag = (overrides: Partial<TagResponseDto> = {}): TagResponseDto => {
|
||||
};
|
||||
};
|
||||
|
||||
const face = (overrides: Partial<AssetFaceResponseDto> = {}): AssetFaceResponseDto => ({
|
||||
id: crypto.randomUUID(),
|
||||
imageWidth: 4000,
|
||||
imageHeight: 3000,
|
||||
boundingBoxX1: 1000,
|
||||
boundingBoxY1: 800,
|
||||
boundingBoxX2: 1600,
|
||||
boundingBoxY2: 1500,
|
||||
person: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const library = (overrides: Partial<LibraryResponseDto> = {}): LibraryResponseDto => {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
name: 'Library',
|
||||
ownerId: adminUserId,
|
||||
assetCount: 0,
|
||||
importPaths: [],
|
||||
exclusionPatterns: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
refreshedAt: null,
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
const apiKey = (overrides: Partial<ApiKeyResponseDto> = {}): ApiKeyResponseDto => {
|
||||
const now = new Date().toISOString();
|
||||
return { id: crypto.randomUUID(), name: 'API Key', permissions: [], createdAt: now, updatedAt: now, ...overrides };
|
||||
};
|
||||
|
||||
const session = (overrides: Partial<SessionResponseDto> = {}): SessionResponseDto => {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
appVersion: '3.0.0',
|
||||
deviceOS: 'Android',
|
||||
deviceType: 'Mobile',
|
||||
current: false,
|
||||
isPendingSyncReset: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
const passkey = (overrides: Partial<PasskeyResponseDto> = {}): PasskeyResponseDto => {
|
||||
const now = new Date().toISOString();
|
||||
return { id: crypto.randomUUID(), name: 'Passkey', createdAt: now, updatedAt: now, usedAt: now, ...overrides };
|
||||
};
|
||||
|
||||
const workflow = (overrides: Partial<WorkflowResponseDto> = {}): WorkflowResponseDto => {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
@@ -580,6 +638,11 @@ export const factory = {
|
||||
tag,
|
||||
sharedLink,
|
||||
workflow,
|
||||
face,
|
||||
library,
|
||||
apiKey,
|
||||
session,
|
||||
passkey,
|
||||
pluginMethod,
|
||||
queue,
|
||||
memories,
|
||||
|
||||
@@ -40,9 +40,36 @@ export const createHandlers = (data: MockData): Handlers => {
|
||||
},
|
||||
getUserStatisticsAdmin: () => ({ json: { images: 0, videos: 0, total: 0 } }),
|
||||
getUserSessionsAdmin: () => ({ json: [] }),
|
||||
getApiKeys: () => ({ json: [] }),
|
||||
getSessions: () => ({ json: [] }),
|
||||
searchPasskeys: () => ({ json: [] }),
|
||||
getAllLibraries: () => ({ json: data.libraryList() }),
|
||||
getLibrary: ({ params }) => {
|
||||
const library = data.getLibrary(params.id);
|
||||
return library ? { json: library } : notFound;
|
||||
},
|
||||
getLibraryStatistics: ({ params }) => {
|
||||
const library = data.getLibrary(params.id);
|
||||
return library
|
||||
? { json: { photos: library.assetCount, videos: 0, total: library.assetCount, usage: 0 } }
|
||||
: notFound;
|
||||
},
|
||||
updateLibrary: ({ params, body }) => {
|
||||
const library = data.updateLibrary(params.id, body);
|
||||
return library ? { json: library } : notFound;
|
||||
},
|
||||
deleteLibrary: ({ params }) => (data.deleteLibrary(params.id) ? { status: 204 } : notFound),
|
||||
getApiKeys: () => ({ json: data.apiKeyList() }),
|
||||
rotateApiKey: ({ params }) => {
|
||||
const rotated = data.rotateApiKey(params.id);
|
||||
return rotated ? { status: 201, json: rotated } : notFound;
|
||||
},
|
||||
deleteApiKey: ({ params }) => (data.deleteApiKey(params.id) ? { status: 204 } : notFound),
|
||||
getSessions: () => ({ json: data.sessionList() }),
|
||||
deleteSession: ({ params }) => (data.deleteSession(params.id) ? { status: 204 } : notFound),
|
||||
deleteAllSessions: () => {
|
||||
data.deleteOtherSessions();
|
||||
return { status: 204 };
|
||||
},
|
||||
searchPasskeys: () => ({ json: data.passkeyList() }),
|
||||
deletePasskey: ({ params }) => (data.deletePasskey(params.id) ? { status: 204 } : notFound),
|
||||
getUserCalendarHeatmapAdmin: ({ params }) => ({
|
||||
json: { from: params.$from ?? '', to: params.to ?? '', series: [], totalCount: 0 },
|
||||
}),
|
||||
@@ -139,7 +166,8 @@ export const createHandlers = (data: MockData): Handlers => {
|
||||
const person = data.updatePerson(params.id, body);
|
||||
return person ? { json: person } : notFound;
|
||||
},
|
||||
getFaces: () => ({ json: [] }),
|
||||
getFaces: ({ params }) => ({ json: data.getFaces(params.id) }),
|
||||
deleteFace: ({ params }) => (data.deleteFace(params.id) ? { status: 204 } : notFound),
|
||||
createFace: () => ({ status: 201 }),
|
||||
getAllPeople: ({ params = {} }) => ({ json: data.peoplePage(params) }),
|
||||
getPersonThumbnail: ({ params }) => ({
|
||||
@@ -169,6 +197,8 @@ export const createHandlers = (data: MockData): Handlers => {
|
||||
data.deleteAlbum(params.id);
|
||||
return { status: 204 };
|
||||
},
|
||||
removeUserFromAlbum: ({ params }) =>
|
||||
data.removeUserFromAlbum(params.id, params.userId) ? { status: 204 } : notFound,
|
||||
getAlbumMapMarkers: ({ params }) => ({ json: data.mapMarkers(params.id) }),
|
||||
getMapMarkers: () => ({ json: data.mapMarkers() }),
|
||||
getAlbumInfo: ({ params }) => {
|
||||
@@ -209,6 +239,7 @@ export const createHandlers = (data: MockData): Handlers => {
|
||||
getAssetsByCity: () => ({ json: data.assetsByCity() }),
|
||||
|
||||
getAllSharedLinks: () => ({ json: data.sharedLinkList() }),
|
||||
removeSharedLink: ({ params }) => (data.removeSharedLink(params.id) ? { status: 204 } : notFound),
|
||||
getMySharedLink: ({ params = {} }) => {
|
||||
const link = data.getSharedLink(params);
|
||||
if (!link) {
|
||||
@@ -235,6 +266,7 @@ export const createHandlers = (data: MockData): Handlers => {
|
||||
}),
|
||||
|
||||
searchWorkflows: () => ({ json: data.workflowList() }),
|
||||
deleteWorkflow: ({ params }) => (data.deleteWorkflow(params.id) ? { status: 204 } : notFound),
|
||||
getWorkflowTriggers: () => ({
|
||||
json: Object.values(WorkflowTrigger).map((trigger) => ({ trigger, types: [WorkflowType.AssetV1] })),
|
||||
}),
|
||||
|
||||
@@ -20,6 +20,7 @@ export const createApp = (page: Page, data: MockData) => {
|
||||
|
||||
return {
|
||||
dialog: page.getByRole('dialog'),
|
||||
dialogButton: (name: string) => page.getByRole('dialog').getByRole('button', { name, exact: true }),
|
||||
toast: (key: string) => page.getByText(key),
|
||||
|
||||
admin: {
|
||||
@@ -37,6 +38,7 @@ export const createApp = (page: Page, data: MockData) => {
|
||||
editButton: button('edit'),
|
||||
deleteButton: button('delete'),
|
||||
resetPasswordButton: button('reset_password'),
|
||||
resetPinCodeButton: button('reset_pin_code'),
|
||||
form: {
|
||||
emailInput: labelled('email'),
|
||||
passwordInput: labelled('password'),
|
||||
@@ -53,6 +55,12 @@ export const createApp = (page: Page, data: MockData) => {
|
||||
createAlbumButton: button('create_album'),
|
||||
},
|
||||
|
||||
library: {
|
||||
deleteButton: button('delete').filter({ hasText: 'delete' }),
|
||||
rowDeleteButton: (text: string) =>
|
||||
page.getByRole('row').filter({ hasText: text }).getByRole('button', { name: 'delete', exact: true }),
|
||||
},
|
||||
|
||||
tags: {
|
||||
child: (name: string) => page.getByRole('main').getByRole('button', { name, exact: true }),
|
||||
},
|
||||
@@ -63,6 +71,10 @@ export const createApp = (page: Page, data: MockData) => {
|
||||
mapButton: labelled('map'),
|
||||
mapDialog: page.getByRole('dialog'),
|
||||
mapMarkers: page.getByRole('dialog').locator('.maplibregl-marker'),
|
||||
optionsMenuButton: labelled('album_options'),
|
||||
optionsMenuItem: page.getByRole('menuitem', { name: 'options', exact: true }),
|
||||
userRoleSelect: (role: string) => page.getByRole('dialog').getByText(role, { exact: true }),
|
||||
removeUserOption: page.getByRole('option', { name: 'remove_user', exact: true }),
|
||||
activity: {
|
||||
commentsButton: page.getByRole('button', { name: 'activity', exact: true }),
|
||||
},
|
||||
@@ -114,6 +126,7 @@ export const createApp = (page: Page, data: MockData) => {
|
||||
heading: (name: string) => page.getByRole('heading', { name }),
|
||||
confirmRemove: () => button('remove').click(),
|
||||
card,
|
||||
deleteButton: labelled('delete_link'),
|
||||
cover: (link: { key: string }) => card(link).getByTestId('album-image'),
|
||||
brokenCover: (link: { key: string }) => card(link).getByTestId('broken-asset'),
|
||||
form: {
|
||||
@@ -202,6 +215,10 @@ const createTimeline = (page: Page, data: MockData) => {
|
||||
await page.getByRole('menuitem', { name: 'unstack', exact: true }).click();
|
||||
},
|
||||
|
||||
openRemoveFromAlbum: async () => {
|
||||
await page.getByLabel('menu', { exact: true }).click();
|
||||
await page.getByRole('menuitem', { name: 'remove_from_album', exact: true }).click();
|
||||
},
|
||||
removeFromAlbum: async () => {
|
||||
await page.getByLabel('menu', { exact: true }).click();
|
||||
await page.getByRole('menuitem', { name: 'remove_from_album', exact: true }).click();
|
||||
@@ -341,6 +358,8 @@ const createViewer = (page: Page) => {
|
||||
},
|
||||
navbarActions: page.getByTestId('asset-viewer-navbar-actions'),
|
||||
tags: page.getByTestId('detail-panel-tags').getByRole('link'),
|
||||
editPeopleButton: page.getByLabel('edit_people', { exact: true }),
|
||||
deleteFaceButton: page.getByLabel('delete_face', { exact: true }),
|
||||
tag: (value: string) => page.getByTestId('detail-panel-tags').getByRole('link', { name: value, exact: true }),
|
||||
addTagButton: page.getByTestId('detail-panel-tags').getByRole('button', { name: 'add_tag', exact: true }),
|
||||
removeTagButton: page.getByTestId('detail-panel-tags').getByRole('button', { name: 'remove_tag', exact: true }),
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Route } from '#lib/route.ts';
|
||||
import { factory } from '#test/common/factories.ts';
|
||||
import { expect, test } from '#test/medium/fixtures.ts';
|
||||
|
||||
test.describe('library administration', () => {
|
||||
test('deletes a library with assets', async ({ page, data, api, app }) => {
|
||||
const library = factory.library({ name: 'Photos', assetCount: 3 });
|
||||
data.addLibraries(library);
|
||||
await page.goto(Route.viewLibrary(library));
|
||||
|
||||
await app.library.deleteButton.click();
|
||||
await app.dialogButton('cancel').click();
|
||||
await expect(app.dialog).toBeHidden();
|
||||
|
||||
await app.library.deleteButton.click();
|
||||
await app.dialogButton('confirm').click();
|
||||
await app.dialogButton('cancel').click();
|
||||
await expect(app.dialog).toBeHidden();
|
||||
expect(api.deleteLibrary).not.toHaveBeenCalled();
|
||||
|
||||
await app.library.deleteButton.click();
|
||||
await app.dialogButton('confirm').click();
|
||||
await app.dialogButton('confirm').click();
|
||||
|
||||
await expect(page).toHaveURL(Route.libraries());
|
||||
expect(api.deleteLibrary).toHaveBeenCalledWith({ params: { id: library.id } });
|
||||
});
|
||||
|
||||
test('removes an import folder', async ({ page, data, api, app }) => {
|
||||
const library = factory.library({ importPaths: ['/photos', '/videos'] });
|
||||
data.addLibraries(library);
|
||||
await page.goto(Route.viewLibrary(library));
|
||||
|
||||
await app.library.rowDeleteButton('/videos').click();
|
||||
await app.dialogButton('cancel').click();
|
||||
await expect(app.dialog).toBeHidden();
|
||||
expect(api.updateLibrary).not.toHaveBeenCalled();
|
||||
|
||||
await app.library.rowDeleteButton('/videos').click();
|
||||
await app.dialogButton('remove').click();
|
||||
|
||||
await expect(app.library.rowDeleteButton('/videos')).toHaveCount(0);
|
||||
expect(api.updateLibrary).toHaveBeenCalledWith({ params: { id: library.id }, body: { importPaths: ['/photos'] } });
|
||||
});
|
||||
|
||||
test('removes an exclusion pattern', async ({ page, data, api, app }) => {
|
||||
const library = factory.library({ exclusionPatterns: ['**/@eaDir/**', '**/.trash/**'] });
|
||||
data.addLibraries(library);
|
||||
await page.goto(Route.viewLibrary(library));
|
||||
|
||||
await app.library.rowDeleteButton('**/.trash/**').click();
|
||||
await app.dialogButton('cancel').click();
|
||||
await expect(app.dialog).toBeHidden();
|
||||
expect(api.updateLibrary).not.toHaveBeenCalled();
|
||||
|
||||
await app.library.rowDeleteButton('**/.trash/**').click();
|
||||
await app.dialogButton('confirm').click();
|
||||
|
||||
await expect(app.library.rowDeleteButton('**/.trash/**')).toHaveCount(0);
|
||||
expect(api.updateLibrary).toHaveBeenCalledWith({
|
||||
params: { id: library.id },
|
||||
body: { exclusionPatterns: ['**/@eaDir/**'] },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -71,4 +71,41 @@ test.describe('user administration', () => {
|
||||
await expect(app.admin.users.deleteButton).toHaveCount(0);
|
||||
await expect(app.admin.users.resetPasswordButton).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('resets a user password', async ({ page, data, api, app }) => {
|
||||
const user = factory.user({ name: 'Reset User', email: 'reset@immich.cloud', isAdmin: false });
|
||||
data.addUsers(user);
|
||||
await page.goto(Route.viewUser(user));
|
||||
|
||||
await app.admin.users.resetPasswordButton.click();
|
||||
await app.dialogButton('cancel').click();
|
||||
await expect(app.dialog).toBeHidden();
|
||||
expect(api.updateUserAdmin).not.toHaveBeenCalled();
|
||||
|
||||
await app.admin.users.resetPasswordButton.click();
|
||||
await app.dialogButton('confirm').click();
|
||||
|
||||
await expect.poll(() => api.updateUserAdmin.calls.length).toBe(1);
|
||||
expect(api.updateUserAdmin).toHaveBeenCalledWith({
|
||||
params: { id: user.id },
|
||||
body: { password: expect.any(String), shouldChangePassword: true },
|
||||
});
|
||||
});
|
||||
|
||||
test('resets a user pin code', async ({ page, data, api, app }) => {
|
||||
const user = factory.user({ name: 'Reset User', email: 'reset@immich.cloud', isAdmin: false });
|
||||
data.addUsers(user);
|
||||
await page.goto(Route.viewUser(user));
|
||||
|
||||
await app.admin.users.resetPinCodeButton.click();
|
||||
await app.dialogButton('cancel').click();
|
||||
await expect(app.dialog).toBeHidden();
|
||||
expect(api.updateUserAdmin).not.toHaveBeenCalled();
|
||||
|
||||
await app.admin.users.resetPinCodeButton.click();
|
||||
await app.dialogButton('confirm').click();
|
||||
|
||||
await expect.poll(() => api.updateUserAdmin.calls.length).toBe(1);
|
||||
expect(api.updateUserAdmin).toHaveBeenCalledWith({ params: { id: user.id }, body: { pinCode: null } });
|
||||
});
|
||||
});
|
||||
@@ -31,6 +31,11 @@ test('removing an asset updates the summary and closes the gap in the timeline',
|
||||
const firstPosition = await app.timeline.position(first);
|
||||
|
||||
await app.timeline.select(first);
|
||||
await app.timeline.selectionBar.openRemoveFromAlbum();
|
||||
await app.dialogButton('cancel').click();
|
||||
await expect(app.dialog).toBeHidden();
|
||||
expect(api.removeAssetFromAlbum).not.toHaveBeenCalled();
|
||||
|
||||
await app.timeline.selectionBar.removeFromAlbum();
|
||||
|
||||
await expect(app.timeline.thumbnail(first)).toHaveCount(0);
|
||||
@@ -58,3 +63,27 @@ test('map stays open after viewing an asset from a marker and going back', async
|
||||
await expect(app.viewer.root).not.toBeVisible();
|
||||
await expect(app.album.mapDialog).toBeVisible();
|
||||
});
|
||||
|
||||
test('removes a user from an album', async ({ page, data, api, app }) => {
|
||||
const viewer = factory.user();
|
||||
const asset = factory.asset();
|
||||
const album = factory.album({ albumUsers: factory.albumUsers({ owner: data.user, viewers: [viewer] }) });
|
||||
data.addAssets(asset);
|
||||
data.addAlbums(album).addToAlbum(album.id, [asset]);
|
||||
await page.goto(Route.viewAlbum({ id: album.id }));
|
||||
await app.album.optionsMenuButton.click();
|
||||
await app.album.optionsMenuItem.click();
|
||||
|
||||
await app.album.userRoleSelect('role_viewer').click();
|
||||
await app.album.removeUserOption.click();
|
||||
await app.dialogButton('cancel').click();
|
||||
await expect(app.dialogButton('cancel')).toHaveCount(0);
|
||||
expect(api.removeUserFromAlbum).not.toHaveBeenCalled();
|
||||
|
||||
await app.album.userRoleSelect('role_viewer').click();
|
||||
await app.album.removeUserOption.click();
|
||||
await app.dialogButton('remove_user').click();
|
||||
|
||||
await expect.poll(() => api.removeUserFromAlbum.calls.length).toBe(1);
|
||||
expect(api.removeUserFromAlbum).toHaveBeenCalledWith({ params: { id: album.id, userId: viewer.id } });
|
||||
});
|
||||
@@ -59,6 +59,11 @@ test.describe('face editor', () => {
|
||||
|
||||
test('tags the selected person after confirming', async ({ api, app }) => {
|
||||
const [alice] = people;
|
||||
await app.viewer.faceEditor.person(alice.name).click();
|
||||
await app.dialogButton('cancel').click();
|
||||
await expect(app.dialog).toBeHidden();
|
||||
expect(api.createFace).not.toHaveBeenCalled();
|
||||
|
||||
await app.viewer.faceEditor.person(alice.name).click();
|
||||
await expect(app.dialog).toBeVisible();
|
||||
await app.viewer.faceEditor.confirmButton.click();
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Route } from '#lib/route.ts';
|
||||
import { factory } from '#test/common/factories.ts';
|
||||
import { expect, test } from '#test/medium/fixtures.ts';
|
||||
|
||||
test('deletes a face', async ({ page, data, api, app }) => {
|
||||
const asset = factory.asset({ width: 4000, height: 3000 });
|
||||
const alice = factory.person({ name: 'Alice Johnson' });
|
||||
const face = factory.face({ person: alice });
|
||||
data.addAssets(asset).addPeople(alice).addFaces(asset.id, face);
|
||||
await page.goto(Route.viewAsset({ id: asset.id }));
|
||||
await app.viewer.waitForAsset(asset);
|
||||
await app.viewer.openDetailPanel();
|
||||
await app.viewer.editPeopleButton.click();
|
||||
|
||||
await app.viewer.deleteFaceButton.click();
|
||||
await app.dialogButton('cancel').click();
|
||||
await expect(app.dialog).toBeHidden();
|
||||
expect(api.deleteFace).not.toHaveBeenCalled();
|
||||
|
||||
await app.viewer.deleteFaceButton.click();
|
||||
await app.dialogButton('confirm').click();
|
||||
|
||||
await expect.poll(() => api.deleteFace.calls.length).toBe(1);
|
||||
expect(api.deleteFace).toHaveBeenCalledWith({ params: { id: face.id }, body: { force: false } });
|
||||
});
|
||||
@@ -115,6 +115,11 @@ test.describe('shared link', () => {
|
||||
await expect(app.timeline.thumbnails).toHaveCount(3);
|
||||
|
||||
await app.timeline.select(first);
|
||||
await app.sharedLink.removeFromSharedLinkButton.click();
|
||||
await app.dialogButton('cancel').click();
|
||||
await expect(app.dialog).toBeHidden();
|
||||
expect(api.removeSharedLinkAssets).not.toHaveBeenCalled();
|
||||
|
||||
await app.sharedLink.removeFromSharedLinkButton.click();
|
||||
await app.sharedLink.confirmRemove();
|
||||
|
||||
@@ -125,4 +130,21 @@ test.describe('shared link', () => {
|
||||
body: { assetIds: [first.id] },
|
||||
});
|
||||
});
|
||||
|
||||
test('deletes a shared link', async ({ page, data, api, app }) => {
|
||||
const link = factory.sharedLink({ type: SharedLinkType.Individual, assets: [factory.asset()] });
|
||||
data.addSharedLinks(link);
|
||||
await page.goto(Route.sharedLinks());
|
||||
|
||||
await app.sharedLink.deleteButton.click();
|
||||
await app.dialogButton('cancel').click();
|
||||
await expect(app.dialog).toBeHidden();
|
||||
expect(api.removeSharedLink).not.toHaveBeenCalled();
|
||||
|
||||
await app.sharedLink.deleteButton.click();
|
||||
await app.dialogButton('delete').click();
|
||||
|
||||
await expect(app.sharedLink.card(link)).toHaveCount(0);
|
||||
expect(api.removeSharedLink).toHaveBeenCalledWith({ params: { id: link.id } });
|
||||
});
|
||||
});
|
||||
@@ -90,6 +90,9 @@ export const createApp = () => {
|
||||
card: (name: string) => main.getByTestId('album-card').filter({ has: page.getByTitle(name, { exact: true }) }),
|
||||
},
|
||||
album: {
|
||||
optionsMenuButton: button('album_options'),
|
||||
deleteMenuItem: page.getByRole('menuitem', { name: 'delete_album', exact: true }),
|
||||
leaveMenuItem: page.getByRole('menuitem', { name: 'leave_album', exact: true }),
|
||||
descriptionInput: page.getByTestId('autogrow-textarea'),
|
||||
commentsButton: button('activity'),
|
||||
likeButton: page.getByRole('button', { name: 'like', exact: true, pressed: false }),
|
||||
@@ -102,6 +105,7 @@ export const createApp = () => {
|
||||
},
|
||||
},
|
||||
confirmButton: page.getByRole('dialog').getByRole('button', { name: 'confirm', exact: true }),
|
||||
dialogButton: (name: string) => page.getByRole('dialog').getByRole('button', { name, exact: true }),
|
||||
cancelButton: page.getByRole('dialog').getByRole('button', { name: 'cancel', exact: true }),
|
||||
duplicates: {
|
||||
keepToggle: (index: number) => button('keep').nth(index),
|
||||
@@ -126,12 +130,23 @@ export const createApp = () => {
|
||||
deleteButton: page.getByRole('dialog').getByRole('button', { name: 'delete', exact: true }),
|
||||
},
|
||||
},
|
||||
workflows: {
|
||||
menuButton: (index: number) => page.getByRole('button', { name: 'open', exact: true }).nth(index),
|
||||
deleteMenuItem: page.getByRole('menuitem', { name: 'delete', exact: true }),
|
||||
},
|
||||
trash: {
|
||||
emptyTrashButton: button('empty_trash'),
|
||||
restoreAllButton: button('restore_all'),
|
||||
},
|
||||
settings: {
|
||||
manageOauthAccountLink: page.getByRole('link', { name: 'manage_oauth_account', exact: true }),
|
||||
section: (title: string) => page.getByRole('button', { name: new RegExp(`^${title}`) }),
|
||||
rotateApiKeyButton: button('rotate_key'),
|
||||
apiKeySecretDoneButton: page.getByRole('dialog').getByRole('button', { name: 'done', exact: true }),
|
||||
deleteApiKeyButton: button('delete_key'),
|
||||
deletePasskeyButton: button('delete_passkey'),
|
||||
logOutDeviceButton: button('log_out'),
|
||||
logOutAllDevicesButton: button('log_out_all_devices'),
|
||||
},
|
||||
people: {
|
||||
nameInput: (index: number) => main.getByPlaceholder('add_a_name').nth(index),
|
||||
|
||||
Reference in new issue
Block a user