mirror of
https://github.com/photoprism/photoprism.git
synced 2026-01-23 02:24:24 +00:00
Tests: Add component tests for navigation, photo toolbar, and file editing
This commit is contained in:
parent
4360fff83c
commit
b91ac2e67e
4 changed files with 1267 additions and 0 deletions
370
frontend/tests/vitest/component/navigation.test.js
Normal file
370
frontend/tests/vitest/component/navigation.test.js
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { shallowMount, config as VTUConfig } from "@vue/test-utils";
|
||||
import PNavigation from "component/navigation.vue";
|
||||
|
||||
function mountNavigation({
|
||||
routeName = "photos",
|
||||
routeMeta = { hideNav: false },
|
||||
isPublic = false,
|
||||
isRestricted = false,
|
||||
sessionAuth = true,
|
||||
featureOverrides = {},
|
||||
configValues = {},
|
||||
allowMock,
|
||||
routerPush,
|
||||
vuetifyDisplay = { smAndDown: false },
|
||||
eventPublish,
|
||||
utilOverrides = {},
|
||||
sessionOverrides = {},
|
||||
} = {}) {
|
||||
const baseConfig = VTUConfig.global.mocks.$config || {};
|
||||
const baseEvent = VTUConfig.global.mocks.$event || {};
|
||||
const baseUtil = VTUConfig.global.mocks.$util || {};
|
||||
const baseNotify = VTUConfig.global.mocks.$notify || {};
|
||||
|
||||
const featureFlags = {
|
||||
files: true,
|
||||
settings: true,
|
||||
upload: true,
|
||||
account: true,
|
||||
logs: true,
|
||||
library: true,
|
||||
places: true,
|
||||
...featureOverrides,
|
||||
};
|
||||
|
||||
const values = {
|
||||
siteUrl: "http://localhost:2342/",
|
||||
usage: { filesTotal: 1024, filesUsed: 512 },
|
||||
legalUrl: configValues.legalUrl ?? null,
|
||||
legalInfo: configValues.legalInfo ?? "",
|
||||
disable: { settings: false },
|
||||
count: {},
|
||||
...configValues,
|
||||
};
|
||||
|
||||
const configMock = {
|
||||
...baseConfig,
|
||||
getName: baseConfig.getName || vi.fn(() => "PhotoPrism"),
|
||||
getAbout: baseConfig.getAbout || vi.fn(() => "About"),
|
||||
getIcon: baseConfig.getIcon || vi.fn(() => "/icon.png"),
|
||||
getTier: baseConfig.getTier || vi.fn(() => 1),
|
||||
isPro: baseConfig.isPro || vi.fn(() => false),
|
||||
isSponsor: baseConfig.isSponsor || vi.fn(() => false),
|
||||
get: vi.fn((key) => {
|
||||
if (key === "demo") return false;
|
||||
if (key === "public") return isPublic;
|
||||
if (key === "readonly") return false;
|
||||
return false;
|
||||
}),
|
||||
feature: vi.fn((name) => {
|
||||
if (name in featureFlags) {
|
||||
return !!featureFlags[name];
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
allow: allowMock || baseConfig.allow || vi.fn(() => true),
|
||||
deny: vi.fn((resource, action) => (resource === "photos" && action === "access_library" ? isRestricted : false)),
|
||||
values,
|
||||
disconnected: false,
|
||||
page: { title: "Photos" },
|
||||
test: false,
|
||||
};
|
||||
|
||||
const session = {
|
||||
auth: sessionAuth,
|
||||
isAdmin: vi.fn(() => true),
|
||||
isSuperAdmin: vi.fn(() => true),
|
||||
hasScope: vi.fn(() => false),
|
||||
getUser: vi.fn(() => ({
|
||||
getDisplayName: vi.fn(() => "Test User"),
|
||||
getAccountInfo: vi.fn(() => "test@example.com"),
|
||||
getAvatarURL: vi.fn(() => "/avatar.jpg"),
|
||||
})),
|
||||
logout: vi.fn(),
|
||||
...sessionOverrides,
|
||||
};
|
||||
|
||||
const publish = eventPublish || baseEvent.publish || vi.fn();
|
||||
|
||||
const eventBus = {
|
||||
...baseEvent,
|
||||
publish,
|
||||
subscribe: baseEvent.subscribe || vi.fn(() => "sub-id"),
|
||||
unsubscribe: baseEvent.unsubscribe || vi.fn(),
|
||||
};
|
||||
|
||||
const notify = {
|
||||
...baseNotify,
|
||||
info: baseNotify.info || vi.fn(),
|
||||
blockUI: baseNotify.blockUI || vi.fn(),
|
||||
};
|
||||
|
||||
const util = {
|
||||
...baseUtil,
|
||||
openExternalUrl: vi.fn(),
|
||||
gigaBytes: vi.fn((bytes) => bytes),
|
||||
...utilOverrides,
|
||||
};
|
||||
|
||||
const push = routerPush || vi.fn();
|
||||
|
||||
const wrapper = shallowMount(PNavigation, {
|
||||
global: {
|
||||
mocks: {
|
||||
$config: configMock,
|
||||
$session: session,
|
||||
$router: { push },
|
||||
$route: { name: routeName, meta: routeMeta },
|
||||
$vuetify: { display: { smAndDown: !!vuetifyDisplay.smAndDown } },
|
||||
$event: eventBus,
|
||||
$util: util,
|
||||
$notify: notify,
|
||||
$isRtl: false,
|
||||
},
|
||||
stubs: {
|
||||
"router-link": { template: "<a><slot /></a>" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
wrapper,
|
||||
configMock,
|
||||
session,
|
||||
eventBus,
|
||||
notify,
|
||||
util,
|
||||
push,
|
||||
};
|
||||
}
|
||||
|
||||
describe("component/navigation", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("routeName", () => {
|
||||
it("returns true when current route starts with given name", () => {
|
||||
const { wrapper } = mountNavigation({ routeName: "photos_browse" });
|
||||
expect(wrapper.vm.routeName("photos")).toBe(true);
|
||||
expect(wrapper.vm.routeName("albums")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when name or route name is missing", () => {
|
||||
const { wrapper } = mountNavigation({ routeName: "" });
|
||||
expect(wrapper.vm.routeName("photos")).toBe(false);
|
||||
expect(wrapper.vm.routeName("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("auth and visibility", () => {
|
||||
it("auth is true when session is authenticated", () => {
|
||||
const { wrapper } = mountNavigation({ sessionAuth: true, isPublic: false });
|
||||
expect(wrapper.vm.auth).toBe(true);
|
||||
});
|
||||
|
||||
it("auth is true when instance is public even without session", () => {
|
||||
const { wrapper } = mountNavigation({ sessionAuth: false, isPublic: true });
|
||||
expect(wrapper.vm.auth).toBe(true);
|
||||
});
|
||||
|
||||
it("auth is false when neither session nor public access is available", () => {
|
||||
const { wrapper } = mountNavigation({ sessionAuth: false, isPublic: false });
|
||||
expect(wrapper.vm.auth).toBe(false);
|
||||
});
|
||||
|
||||
it("visible is false when route meta.hideNav is true", () => {
|
||||
const { wrapper } = mountNavigation({ routeMeta: { hideNav: true } });
|
||||
expect(wrapper.vm.visible).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("drawer behavior", () => {
|
||||
it("toggleDrawer toggles drawer on small screens", () => {
|
||||
const { wrapper } = mountNavigation({
|
||||
vuetifyDisplay: { smAndDown: true },
|
||||
sessionAuth: true,
|
||||
});
|
||||
|
||||
// Force small-screen mode and authenticated session
|
||||
wrapper.vm.$vuetify.display.smAndDown = true;
|
||||
wrapper.vm.session.auth = true;
|
||||
wrapper.vm.isPublic = false;
|
||||
|
||||
wrapper.vm.drawer = false;
|
||||
wrapper.vm.toggleDrawer({ target: {} });
|
||||
expect(wrapper.vm.drawer).toBe(true);
|
||||
|
||||
wrapper.vm.toggleDrawer({ target: {} });
|
||||
expect(wrapper.vm.drawer).toBe(false);
|
||||
});
|
||||
|
||||
it("toggleDrawer toggles mini mode on desktop", () => {
|
||||
const { wrapper } = mountNavigation({
|
||||
vuetifyDisplay: { smAndDown: false },
|
||||
isRestricted: false,
|
||||
});
|
||||
|
||||
const initial = wrapper.vm.isMini;
|
||||
wrapper.vm.toggleDrawer({ target: {} });
|
||||
expect(wrapper.vm.isMini).toBe(!initial);
|
||||
});
|
||||
|
||||
it("toggleIsMini respects restricted mode and updates localStorage", () => {
|
||||
const setItemSpy = vi.spyOn(Storage.prototype, "setItem");
|
||||
|
||||
const { wrapper } = mountNavigation({ isRestricted: false });
|
||||
const initial = wrapper.vm.isMini;
|
||||
|
||||
wrapper.vm.toggleIsMini();
|
||||
expect(wrapper.vm.isMini).toBe(!initial);
|
||||
expect(setItemSpy).toHaveBeenCalledWith("navigation.mode", `${!initial}`);
|
||||
|
||||
wrapper.vm.isRestricted = true;
|
||||
const before = wrapper.vm.isMini;
|
||||
wrapper.vm.toggleIsMini();
|
||||
expect(wrapper.vm.isMini).toBe(before);
|
||||
});
|
||||
});
|
||||
|
||||
describe("account and legal navigation", () => {
|
||||
it("showAccountSettings routes to account settings when account feature is enabled", () => {
|
||||
const { wrapper, push } = mountNavigation({
|
||||
featureOverrides: { account: true },
|
||||
});
|
||||
|
||||
wrapper.vm.showAccountSettings();
|
||||
expect(push).toHaveBeenCalledWith({ name: "settings_account" });
|
||||
});
|
||||
|
||||
it("showAccountSettings falls back to general settings when account feature is disabled", () => {
|
||||
const { wrapper, push } = mountNavigation({
|
||||
featureOverrides: { account: false },
|
||||
});
|
||||
|
||||
wrapper.vm.showAccountSettings();
|
||||
expect(push).toHaveBeenCalledWith({ name: "settings" });
|
||||
});
|
||||
|
||||
it("showLegalInfo opens external URL when legalUrl is configured", () => {
|
||||
const { wrapper, util } = mountNavigation({
|
||||
configValues: { legalUrl: "https://example.com/legal" },
|
||||
});
|
||||
|
||||
wrapper.vm.showLegalInfo();
|
||||
expect(util.openExternalUrl).toHaveBeenCalledWith("https://example.com/legal");
|
||||
});
|
||||
|
||||
it("showLegalInfo routes to about page when legalUrl is missing", () => {
|
||||
const { wrapper, push } = mountNavigation({
|
||||
configValues: { legalUrl: null },
|
||||
});
|
||||
|
||||
wrapper.vm.showLegalInfo();
|
||||
expect(push).toHaveBeenCalledWith({ name: "about" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("home and upload actions", () => {
|
||||
it("onHome toggles drawer on small screens and does not navigate", () => {
|
||||
const { wrapper, push } = mountNavigation({
|
||||
vuetifyDisplay: { smAndDown: true },
|
||||
routeName: "browse",
|
||||
});
|
||||
|
||||
// Ensure mobile mode and authenticated session so drawer logic runs
|
||||
wrapper.vm.$vuetify.display.smAndDown = true;
|
||||
wrapper.vm.session.auth = true;
|
||||
wrapper.vm.isPublic = false;
|
||||
wrapper.vm.drawer = false;
|
||||
|
||||
wrapper.vm.onHome({ target: {} });
|
||||
expect(wrapper.vm.drawer).toBe(true);
|
||||
expect(push).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("onHome navigates to home on desktop when not already there", () => {
|
||||
const { wrapper, push } = mountNavigation({
|
||||
vuetifyDisplay: { smAndDown: false },
|
||||
routeName: "albums",
|
||||
});
|
||||
|
||||
// Force desktop mode explicitly to avoid relying on Vuetify defaults
|
||||
wrapper.vm.$vuetify.display.smAndDown = false;
|
||||
|
||||
wrapper.vm.onHome({ target: {} });
|
||||
expect(push).toHaveBeenCalledWith({ name: "home" });
|
||||
});
|
||||
|
||||
it("openUpload publishes dialog.upload event", () => {
|
||||
const publish = vi.fn();
|
||||
const { wrapper, eventBus } = mountNavigation({ eventPublish: publish });
|
||||
|
||||
wrapper.vm.openUpload();
|
||||
expect(eventBus.publish).toHaveBeenCalledWith("dialog.upload");
|
||||
});
|
||||
});
|
||||
|
||||
describe("info and usage actions", () => {
|
||||
it("reloadApp shows info notification and blocks UI", () => {
|
||||
vi.useFakeTimers();
|
||||
const { wrapper, notify } = mountNavigation();
|
||||
const setTimeoutSpy = vi.spyOn(global, "setTimeout");
|
||||
|
||||
wrapper.vm.reloadApp();
|
||||
|
||||
expect(notify.info).toHaveBeenCalledWith("Reloading…");
|
||||
expect(notify.blockUI).toHaveBeenCalled();
|
||||
expect(setTimeoutSpy).toHaveBeenCalled();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("showUsageInfo routes to index files", () => {
|
||||
const { wrapper, push } = mountNavigation();
|
||||
wrapper.vm.showUsageInfo();
|
||||
expect(push).toHaveBeenCalledWith({ path: "/index/files" });
|
||||
});
|
||||
|
||||
it("showServerConnectionHelp routes to websockets help", () => {
|
||||
const { wrapper, push } = mountNavigation();
|
||||
wrapper.vm.showServerConnectionHelp();
|
||||
expect(push).toHaveBeenCalledWith({ path: "/help/websockets" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("indexing state", () => {
|
||||
it("onIndex sets indexing true for file, folder and indexing events", () => {
|
||||
const { wrapper } = mountNavigation();
|
||||
|
||||
wrapper.vm.onIndex("index.file");
|
||||
expect(wrapper.vm.indexing).toBe(true);
|
||||
|
||||
wrapper.vm.onIndex("index.folder");
|
||||
expect(wrapper.vm.indexing).toBe(true);
|
||||
|
||||
wrapper.vm.onIndex("index.indexing");
|
||||
expect(wrapper.vm.indexing).toBe(true);
|
||||
});
|
||||
|
||||
it("onIndex sets indexing false when completed", () => {
|
||||
const { wrapper } = mountNavigation();
|
||||
|
||||
wrapper.vm.indexing = true;
|
||||
wrapper.vm.onIndex("index.completed");
|
||||
expect(wrapper.vm.indexing).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("logout", () => {
|
||||
it("onLogout calls session.logout", () => {
|
||||
const logout = vi.fn();
|
||||
const { wrapper, session } = mountNavigation({
|
||||
sessionOverrides: { logout },
|
||||
});
|
||||
|
||||
wrapper.vm.onLogout();
|
||||
expect(session.logout).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
325
frontend/tests/vitest/component/photo/edit/files.test.js
Normal file
325
frontend/tests/vitest/component/photo/edit/files.test.js
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { shallowMount, config as VTUConfig } from "@vue/test-utils";
|
||||
import PTabPhotoFiles from "component/photo/edit/files.vue";
|
||||
import Thumb from "model/thumb";
|
||||
|
||||
function createFile(overrides = {}) {
|
||||
return {
|
||||
UID: "file-uid",
|
||||
Name: "2018/01/dir:with#hash/file.jpg",
|
||||
FileType: "jpg",
|
||||
Error: "",
|
||||
Primary: false,
|
||||
Sidecar: false,
|
||||
Root: "/",
|
||||
Missing: false,
|
||||
Pages: 0,
|
||||
Frames: 0,
|
||||
Duration: 0,
|
||||
FPS: 0,
|
||||
Hash: "hash123",
|
||||
OriginalName: "file.jpg",
|
||||
ColorProfile: "",
|
||||
MainColor: "",
|
||||
Chroma: 0,
|
||||
CreatedAt: "2023-01-01T12:00:00Z",
|
||||
CreatedIn: 1000,
|
||||
UpdatedAt: "2023-01-02T12:00:00Z",
|
||||
UpdatedIn: 2000,
|
||||
thumbnailUrl: vi.fn(() => "/thumb/file.jpg"),
|
||||
storageInfo: vi.fn(() => "local"),
|
||||
typeInfo: vi.fn(() => "JPEG"),
|
||||
sizeInfo: vi.fn(() => "1 MB"),
|
||||
isAnimated: vi.fn(() => false),
|
||||
baseName: vi.fn(() => "file.jpg"),
|
||||
download: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function mountPhotoFiles({
|
||||
fileOverrides = {},
|
||||
featuresOverrides = {},
|
||||
experimental = false,
|
||||
isMobile = false,
|
||||
modelOverrides = {},
|
||||
routerOverrides = {},
|
||||
} = {}) {
|
||||
const baseConfig = VTUConfig.global.mocks.$config || {};
|
||||
const baseSettings = baseConfig.getSettings ? baseConfig.getSettings() : { features: {} };
|
||||
|
||||
const features = {
|
||||
...(baseSettings.features || {}),
|
||||
download: true,
|
||||
edit: true,
|
||||
delete: true,
|
||||
...featuresOverrides,
|
||||
};
|
||||
|
||||
const configMock = {
|
||||
...baseConfig,
|
||||
getSettings: vi.fn(() => ({
|
||||
...baseSettings,
|
||||
features,
|
||||
})),
|
||||
get: vi.fn((key) => {
|
||||
if (key === "experimental") return experimental;
|
||||
if (baseConfig.get) {
|
||||
return baseConfig.get(key);
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
getTimeZone: baseConfig.getTimeZone || vi.fn(() => "UTC"),
|
||||
allow: baseConfig.allow || vi.fn(() => true),
|
||||
values: baseConfig.values || {},
|
||||
};
|
||||
|
||||
const file = createFile(fileOverrides);
|
||||
|
||||
const model = {
|
||||
fileModels: vi.fn(() => [file]),
|
||||
deleteFile: vi.fn(() => Promise.resolve()),
|
||||
unstackFile: vi.fn(),
|
||||
setPrimaryFile: vi.fn(),
|
||||
changeFileOrientation: vi.fn(() => Promise.resolve()),
|
||||
...modelOverrides,
|
||||
};
|
||||
|
||||
const router = {
|
||||
push: vi.fn(),
|
||||
resolve: vi.fn((route) => ({ href: route.path || "" })),
|
||||
...routerOverrides,
|
||||
};
|
||||
|
||||
const lightbox = {
|
||||
openModels: vi.fn(),
|
||||
};
|
||||
|
||||
const baseUtil = VTUConfig.global.mocks.$util || {};
|
||||
const util = {
|
||||
...baseUtil,
|
||||
openUrl: vi.fn(),
|
||||
formatDuration: baseUtil.formatDuration || vi.fn((d) => String(d)),
|
||||
fileType: baseUtil.fileType || vi.fn((t) => t),
|
||||
codecName: baseUtil.codecName || vi.fn((c) => c),
|
||||
formatNs: baseUtil.formatNs || vi.fn((n) => String(n)),
|
||||
};
|
||||
|
||||
const wrapper = shallowMount(PTabPhotoFiles, {
|
||||
props: {
|
||||
uid: "photo-uid",
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$config: configMock,
|
||||
$view: {
|
||||
getData: () => ({
|
||||
model,
|
||||
}),
|
||||
},
|
||||
$router: router,
|
||||
$lightbox: lightbox,
|
||||
$util: util,
|
||||
$isMobile: isMobile,
|
||||
$gettext: VTUConfig.global.mocks.$gettext || ((s) => s),
|
||||
$notify: VTUConfig.global.mocks.$notify,
|
||||
$isRtl: false,
|
||||
},
|
||||
stubs: {
|
||||
"p-file-delete-dialog": true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
wrapper,
|
||||
file,
|
||||
model,
|
||||
router,
|
||||
lightbox,
|
||||
util,
|
||||
configMock,
|
||||
};
|
||||
}
|
||||
|
||||
describe("component/photo/edit/files", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("action buttons visibility", () => {
|
||||
it("shows download, primary, unstack and delete buttons for editable JPG file", () => {
|
||||
const { wrapper } = mountPhotoFiles({
|
||||
fileOverrides: { FileType: "jpg", Primary: false, Sidecar: false, Root: "/", Error: "" },
|
||||
featuresOverrides: { download: true, edit: true, delete: true },
|
||||
});
|
||||
|
||||
const file = wrapper.vm.view.model.fileModels()[0];
|
||||
const { features, experimental, canAccessPrivate } = wrapper.vm;
|
||||
|
||||
// Download button conditions
|
||||
expect(features.download).toBe(true);
|
||||
// Primary button conditions
|
||||
expect(features.edit && (file.FileType === "jpg" || file.FileType === "png") && !file.Error && !file.Primary).toBe(true);
|
||||
// Unstack button conditions
|
||||
expect(features.edit && !file.Sidecar && !file.Error && !file.Primary && file.Root === "/").toBe(true);
|
||||
// Delete button conditions
|
||||
expect(features.delete && !file.Primary).toBe(true);
|
||||
// Browse button should not be visible in this scenario
|
||||
expect(experimental && canAccessPrivate && file.Primary).toBe(false);
|
||||
});
|
||||
|
||||
it("shows browse button only for primary file when experimental and private access are enabled", () => {
|
||||
const { wrapper } = mountPhotoFiles({
|
||||
fileOverrides: { Primary: true, Root: "/", FileType: "jpg" },
|
||||
experimental: true,
|
||||
});
|
||||
|
||||
const file = wrapper.vm.view.model.fileModels()[0];
|
||||
const { features, experimental, canAccessPrivate } = wrapper.vm;
|
||||
|
||||
// Browse button conditions
|
||||
expect(experimental && canAccessPrivate && file.Primary).toBe(true);
|
||||
// Other actions should not be available for primary file in this scenario
|
||||
expect(features.edit && (file.FileType === "jpg" || file.FileType === "png") && !file.Error && !file.Primary).toBe(false);
|
||||
expect(features.edit && !file.Sidecar && !file.Error && !file.Primary && file.Root === "/").toBe(false);
|
||||
expect(features.delete && !file.Primary).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("openFile", () => {
|
||||
it("opens file in lightbox using Thumb.fromFile", () => {
|
||||
const thumbModel = {};
|
||||
const { wrapper, file, model, lightbox } = mountPhotoFiles();
|
||||
const thumbSpy = vi.spyOn(Thumb, "fromFile").mockReturnValue(thumbModel);
|
||||
|
||||
wrapper.vm.openFile(file);
|
||||
|
||||
expect(thumbSpy).toHaveBeenCalledWith(model, file);
|
||||
expect(lightbox.openModels).toHaveBeenCalledWith([thumbModel], 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("openFolder", () => {
|
||||
it("emits close and navigates via router.push on mobile", () => {
|
||||
const { wrapper, router, util, file } = mountPhotoFiles({
|
||||
isMobile: true,
|
||||
fileOverrides: { Name: "2018/01/file.jpg" },
|
||||
});
|
||||
|
||||
wrapper.vm.openFolder(file);
|
||||
|
||||
expect(wrapper.emitted("close")).toBeTruthy();
|
||||
expect(router.push).toHaveBeenCalledWith({ path: "/index/files/2018/01" });
|
||||
expect(util.openUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("opens folder in new tab on desktop with encoded path", () => {
|
||||
const encodedPath = "/index/files/2018/01/dir%3Awith%23hash";
|
||||
const resolve = vi.fn((route) => ({ href: route.path }));
|
||||
const { wrapper, util, file } = mountPhotoFiles({
|
||||
isMobile: false,
|
||||
routerOverrides: { resolve },
|
||||
fileOverrides: { Name: "2018/01/dir:with#hash/file.jpg" },
|
||||
});
|
||||
|
||||
wrapper.vm.openFolder(file);
|
||||
|
||||
expect(resolve).toHaveBeenCalledWith({ path: encodedPath });
|
||||
expect(util.openUrl).toHaveBeenCalledWith(encodedPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe("file actions", () => {
|
||||
it("downloadFile shows notification and calls file.download", async () => {
|
||||
const { wrapper, file } = mountPhotoFiles();
|
||||
const { default: notifyModule } = await import("common/notify");
|
||||
const notifySpy = vi.spyOn(notifyModule, "success");
|
||||
|
||||
wrapper.vm.downloadFile(file);
|
||||
|
||||
expect(notifySpy).toHaveBeenCalledWith("Downloading…");
|
||||
expect(file.download).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("unstackFile and setPrimaryFile delegate to model when file is present", () => {
|
||||
const unstackSpy = vi.fn();
|
||||
const setPrimarySpy = vi.fn();
|
||||
const { wrapper, file } = mountPhotoFiles({
|
||||
modelOverrides: {
|
||||
unstackFile: unstackSpy,
|
||||
setPrimaryFile: setPrimarySpy,
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm.unstackFile(file);
|
||||
wrapper.vm.setPrimaryFile(file);
|
||||
|
||||
expect(unstackSpy).toHaveBeenCalledWith(file.UID);
|
||||
expect(setPrimarySpy).toHaveBeenCalledWith(file.UID);
|
||||
|
||||
unstackSpy.mockClear();
|
||||
setPrimarySpy.mockClear();
|
||||
|
||||
wrapper.vm.unstackFile(null);
|
||||
wrapper.vm.setPrimaryFile(null);
|
||||
|
||||
expect(unstackSpy).not.toHaveBeenCalled();
|
||||
expect(setPrimarySpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("confirmDeleteFile calls model.deleteFile and closes dialog", async () => {
|
||||
const deleteFileSpy = vi.fn(() => Promise.resolve());
|
||||
const { wrapper, file } = mountPhotoFiles({
|
||||
modelOverrides: {
|
||||
deleteFile: deleteFileSpy,
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm.deleteFile.dialog = true;
|
||||
wrapper.vm.deleteFile.file = file;
|
||||
|
||||
await wrapper.vm.confirmDeleteFile();
|
||||
|
||||
expect(deleteFileSpy).toHaveBeenCalledWith(file.UID);
|
||||
expect(wrapper.vm.deleteFile.dialog).toBe(false);
|
||||
expect(wrapper.vm.deleteFile.file).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("changeOrientation", () => {
|
||||
it("calls model.changeFileOrientation and shows success message", async () => {
|
||||
const changeOrientationSpy = vi.fn(() => Promise.resolve());
|
||||
const { wrapper, file } = mountPhotoFiles({
|
||||
modelOverrides: {
|
||||
changeFileOrientation: changeOrientationSpy,
|
||||
},
|
||||
});
|
||||
|
||||
const notifySuccessSpy = vi.spyOn(wrapper.vm.$notify, "success");
|
||||
|
||||
wrapper.vm.changeOrientation(file);
|
||||
expect(wrapper.vm.busy).toBe(true);
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
expect(changeOrientationSpy).toHaveBeenCalledWith(file);
|
||||
expect(notifySuccessSpy).toHaveBeenCalledWith("Changes successfully saved");
|
||||
expect(wrapper.vm.busy).toBe(false);
|
||||
});
|
||||
|
||||
it("does nothing when file is missing", () => {
|
||||
const changeOrientationSpy = vi.fn(() => Promise.resolve());
|
||||
const { wrapper } = mountPhotoFiles({
|
||||
modelOverrides: {
|
||||
changeFileOrientation: changeOrientationSpy,
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm.changeOrientation(null);
|
||||
|
||||
expect(changeOrientationSpy).not.toHaveBeenCalled();
|
||||
expect(wrapper.vm.busy).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
226
frontend/tests/vitest/component/photo/edit/labels.test.js
Normal file
226
frontend/tests/vitest/component/photo/edit/labels.test.js
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { shallowMount, config as VTUConfig } from "@vue/test-utils";
|
||||
import PTabPhotoLabels from "component/photo/edit/labels.vue";
|
||||
import Thumb from "model/thumb";
|
||||
|
||||
function mountPhotoLabels({ modelOverrides = {}, routerOverrides = {}, utilOverrides = {}, notifyOverrides = {}, viewHasModel = true } = {}) {
|
||||
const baseConfig = VTUConfig.global.mocks.$config || {};
|
||||
const baseNotify = VTUConfig.global.mocks.$notify || {};
|
||||
const baseUtil = VTUConfig.global.mocks.$util || {};
|
||||
|
||||
const model = viewHasModel
|
||||
? {
|
||||
removeLabel: vi.fn(() => Promise.resolve()),
|
||||
addLabel: vi.fn(() => Promise.resolve()),
|
||||
activateLabel: vi.fn(),
|
||||
...modelOverrides,
|
||||
}
|
||||
: null;
|
||||
|
||||
const router = {
|
||||
push: vi.fn(() => Promise.resolve()),
|
||||
...routerOverrides,
|
||||
};
|
||||
|
||||
const util = {
|
||||
...baseUtil,
|
||||
sourceName: vi.fn((s) => `source-${s}`),
|
||||
...utilOverrides,
|
||||
};
|
||||
|
||||
const notify = {
|
||||
...baseNotify,
|
||||
success: baseNotify.success || vi.fn(),
|
||||
error: baseNotify.error || vi.fn(),
|
||||
warn: baseNotify.warn || vi.fn(),
|
||||
...notifyOverrides,
|
||||
};
|
||||
|
||||
const lightbox = {
|
||||
openModels: vi.fn(),
|
||||
};
|
||||
|
||||
const wrapper = shallowMount(PTabPhotoLabels, {
|
||||
props: {
|
||||
uid: "photo-uid",
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$config: baseConfig,
|
||||
$view: {
|
||||
getData: () => ({
|
||||
model,
|
||||
}),
|
||||
},
|
||||
$router: router,
|
||||
$util: util,
|
||||
$notify: notify,
|
||||
$lightbox: lightbox,
|
||||
$gettext: VTUConfig.global.mocks.$gettext || ((s) => s),
|
||||
$isRtl: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
wrapper,
|
||||
model,
|
||||
router,
|
||||
util,
|
||||
notify,
|
||||
lightbox,
|
||||
};
|
||||
}
|
||||
|
||||
describe("component/photo/edit/labels", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("sourceName", () => {
|
||||
it("delegates to $util.sourceName", () => {
|
||||
const sourceNameSpy = vi.fn(() => "Human");
|
||||
const { wrapper, util } = mountPhotoLabels({
|
||||
utilOverrides: { sourceName: sourceNameSpy },
|
||||
});
|
||||
|
||||
const result = wrapper.vm.sourceName("auto");
|
||||
|
||||
expect(sourceNameSpy).toHaveBeenCalledWith("auto");
|
||||
expect(result).toBe("Human");
|
||||
// Ensure util on instance is the same object so we actually spied on the right method
|
||||
expect(wrapper.vm.$util).toBe(util);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeLabel", () => {
|
||||
it("does nothing when label is missing", () => {
|
||||
const removeSpy = vi.fn(() => Promise.resolve());
|
||||
const { wrapper } = mountPhotoLabels({
|
||||
modelOverrides: { removeLabel: removeSpy },
|
||||
});
|
||||
|
||||
wrapper.vm.removeLabel(null);
|
||||
|
||||
expect(removeSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls model.removeLabel and shows success message", async () => {
|
||||
const removeSpy = vi.fn(() => Promise.resolve());
|
||||
const notifySuccessSpy = vi.fn();
|
||||
const { wrapper } = mountPhotoLabels({
|
||||
modelOverrides: { removeLabel: removeSpy },
|
||||
notifyOverrides: { success: notifySuccessSpy },
|
||||
});
|
||||
|
||||
const label = { ID: 5, Name: "Cat" };
|
||||
|
||||
wrapper.vm.removeLabel(label);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(removeSpy).toHaveBeenCalledWith(5);
|
||||
expect(notifySuccessSpy).toHaveBeenCalledWith("removed Cat");
|
||||
});
|
||||
});
|
||||
|
||||
describe("addLabel", () => {
|
||||
it("does nothing when newLabel is empty", () => {
|
||||
const addSpy = vi.fn(() => Promise.resolve());
|
||||
const { wrapper } = mountPhotoLabels({
|
||||
modelOverrides: { addLabel: addSpy },
|
||||
});
|
||||
|
||||
wrapper.vm.newLabel = "";
|
||||
wrapper.vm.addLabel();
|
||||
|
||||
expect(addSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls model.addLabel, shows success message and clears newLabel", async () => {
|
||||
const addSpy = vi.fn(() => Promise.resolve());
|
||||
const notifySuccessSpy = vi.fn();
|
||||
const { wrapper } = mountPhotoLabels({
|
||||
modelOverrides: { addLabel: addSpy },
|
||||
notifyOverrides: { success: notifySuccessSpy },
|
||||
});
|
||||
|
||||
wrapper.vm.newLabel = "Dog";
|
||||
wrapper.vm.addLabel();
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
expect(addSpy).toHaveBeenCalledWith("Dog");
|
||||
expect(notifySuccessSpy).toHaveBeenCalledWith("added Dog");
|
||||
expect(wrapper.vm.newLabel).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("activateLabel", () => {
|
||||
it("does nothing when label is missing", () => {
|
||||
const activateSpy = vi.fn();
|
||||
const { wrapper } = mountPhotoLabels({
|
||||
modelOverrides: { activateLabel: activateSpy },
|
||||
});
|
||||
|
||||
wrapper.vm.activateLabel(null);
|
||||
|
||||
expect(activateSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("delegates to model.activateLabel for valid label", () => {
|
||||
const activateSpy = vi.fn();
|
||||
const { wrapper } = mountPhotoLabels({
|
||||
modelOverrides: { activateLabel: activateSpy },
|
||||
});
|
||||
|
||||
const label = { ID: 7, Name: "Summer" };
|
||||
|
||||
wrapper.vm.activateLabel(label);
|
||||
|
||||
expect(activateSpy).toHaveBeenCalledWith(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe("searchLabel", () => {
|
||||
it("navigates to all route with label query and emits close", () => {
|
||||
const push = vi.fn(() => Promise.resolve());
|
||||
const { wrapper, router } = mountPhotoLabels({
|
||||
routerOverrides: { push },
|
||||
});
|
||||
|
||||
const label = { Slug: "animals" };
|
||||
|
||||
wrapper.vm.searchLabel(label);
|
||||
|
||||
expect(router.push).toHaveBeenCalledWith({
|
||||
name: "all",
|
||||
query: { q: "label:animals" },
|
||||
});
|
||||
expect(wrapper.emitted("close")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("openPhoto", () => {
|
||||
it("opens photo in lightbox using Thumb.fromPhotos when model is present", () => {
|
||||
const thumbModel = {};
|
||||
const fromPhotosSpy = vi.spyOn(Thumb, "fromPhotos").mockReturnValue([thumbModel]);
|
||||
|
||||
const { wrapper, model, lightbox } = mountPhotoLabels();
|
||||
|
||||
wrapper.vm.openPhoto();
|
||||
|
||||
expect(fromPhotosSpy).toHaveBeenCalledWith([model]);
|
||||
expect(lightbox.openModels).toHaveBeenCalledWith([thumbModel], 0);
|
||||
});
|
||||
|
||||
it("does nothing when model is missing", () => {
|
||||
const fromPhotosSpy = vi.spyOn(Thumb, "fromPhotos").mockReturnValue([]);
|
||||
const { wrapper, lightbox } = mountPhotoLabels({ viewHasModel: false });
|
||||
|
||||
wrapper.vm.openPhoto();
|
||||
|
||||
expect(fromPhotosSpy).not.toHaveBeenCalled();
|
||||
expect(lightbox.openModels).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
346
frontend/tests/vitest/component/photo/toolbar.test.js
Normal file
346
frontend/tests/vitest/component/photo/toolbar.test.js
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { shallowMount, config as VTUConfig } from "@vue/test-utils";
|
||||
import PPhotoToolbar from "component/photo/toolbar.vue";
|
||||
import * as contexts from "options/contexts";
|
||||
import "../../fixtures";
|
||||
|
||||
function mountToolbar({
|
||||
context = contexts.Photos,
|
||||
embedded = false,
|
||||
filter = {
|
||||
q: "",
|
||||
country: "",
|
||||
camera: 0,
|
||||
year: 0,
|
||||
month: 0,
|
||||
color: "",
|
||||
label: "",
|
||||
order: "newest",
|
||||
latlng: null,
|
||||
},
|
||||
staticFilter = {},
|
||||
settings = { view: "mosaic" },
|
||||
featuresOverrides = {},
|
||||
searchOverrides = {},
|
||||
allowMock,
|
||||
refresh = vi.fn(),
|
||||
updateFilter = vi.fn(),
|
||||
updateQuery = vi.fn(),
|
||||
eventPublish,
|
||||
routerOverrides = {},
|
||||
clipboard,
|
||||
openUrlSpy,
|
||||
} = {}) {
|
||||
const baseConfig = VTUConfig.global.mocks.$config;
|
||||
const baseSettings = baseConfig.getSettings ? baseConfig.getSettings() : { features: {} };
|
||||
|
||||
const features = {
|
||||
...(baseSettings.features || {}),
|
||||
upload: true,
|
||||
delete: true,
|
||||
settings: true,
|
||||
...featuresOverrides,
|
||||
};
|
||||
|
||||
const search = {
|
||||
listView: true,
|
||||
...searchOverrides,
|
||||
};
|
||||
|
||||
const configMock = {
|
||||
...baseConfig,
|
||||
getSettings: vi.fn(() => ({
|
||||
...baseSettings,
|
||||
features,
|
||||
search,
|
||||
})),
|
||||
allow: allowMock || vi.fn(() => true),
|
||||
values: {
|
||||
countries: [],
|
||||
cameras: [],
|
||||
categories: [],
|
||||
...(baseConfig.values || {}),
|
||||
},
|
||||
};
|
||||
|
||||
const publish = eventPublish || vi.fn();
|
||||
|
||||
const router = {
|
||||
push: vi.fn(),
|
||||
resolve: vi.fn((route) => ({
|
||||
href: `/library/${route.name || "browse"}`,
|
||||
})),
|
||||
...routerOverrides,
|
||||
};
|
||||
|
||||
const clipboardMock =
|
||||
clipboard ||
|
||||
{
|
||||
clear: vi.fn(),
|
||||
};
|
||||
|
||||
const baseUtil = VTUConfig.global.mocks.$util || {};
|
||||
const util = {
|
||||
...baseUtil,
|
||||
openUrl: openUrlSpy || vi.fn(),
|
||||
};
|
||||
|
||||
const wrapper = shallowMount(PPhotoToolbar, {
|
||||
props: {
|
||||
context,
|
||||
filter,
|
||||
staticFilter,
|
||||
settings,
|
||||
embedded,
|
||||
refresh,
|
||||
updateFilter,
|
||||
updateQuery,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$config: configMock,
|
||||
$session: { isSuperAdmin: vi.fn(() => false) },
|
||||
$event: {
|
||||
...(VTUConfig.global.mocks.$event || {}),
|
||||
publish,
|
||||
},
|
||||
$router: router,
|
||||
$clipboard: clipboardMock,
|
||||
$util: util,
|
||||
},
|
||||
stubs: {
|
||||
PActionMenu: true,
|
||||
PConfirmDialog: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
wrapper,
|
||||
configMock,
|
||||
publish,
|
||||
router,
|
||||
clipboard: clipboardMock,
|
||||
refresh,
|
||||
updateFilter,
|
||||
updateQuery,
|
||||
openUrl: util.openUrl,
|
||||
};
|
||||
}
|
||||
|
||||
describe("component/photo/toolbar", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("menuActions", () => {
|
||||
it("shows upload and docs actions for photos context when upload is allowed", () => {
|
||||
const { wrapper } = mountToolbar();
|
||||
|
||||
const actions = wrapper.vm.menuActions();
|
||||
const byName = (name) => actions.find((a) => a.name === name);
|
||||
|
||||
const refreshAction = byName("refresh");
|
||||
const uploadAction = byName("upload");
|
||||
const docsAction = byName("docs");
|
||||
const troubleshootingAction = byName("troubleshooting");
|
||||
|
||||
expect(refreshAction).toBeDefined();
|
||||
expect(uploadAction).toBeDefined();
|
||||
expect(docsAction).toBeDefined();
|
||||
expect(troubleshootingAction).toBeDefined();
|
||||
|
||||
expect(refreshAction.visible).toBe(true);
|
||||
expect(uploadAction.visible).toBe(true);
|
||||
expect(docsAction.visible).toBe(true);
|
||||
expect(troubleshootingAction.visible).toBe(false);
|
||||
});
|
||||
|
||||
it("hides upload action in archive and hidden contexts", () => {
|
||||
const { wrapper: archiveWrapper } = mountToolbar({ context: contexts.Archive });
|
||||
const archiveActions = archiveWrapper.vm.menuActions();
|
||||
const archiveUpload = archiveActions.find((a) => a.name === "upload");
|
||||
const archiveDocs = archiveActions.find((a) => a.name === "docs");
|
||||
const archiveTroubleshooting = archiveActions.find((a) => a.name === "troubleshooting");
|
||||
|
||||
expect(archiveUpload).toBeDefined();
|
||||
expect(archiveDocs).toBeDefined();
|
||||
expect(archiveTroubleshooting).toBeDefined();
|
||||
|
||||
expect(archiveUpload.visible).toBe(false);
|
||||
expect(archiveDocs.visible).toBe(true);
|
||||
expect(archiveTroubleshooting.visible).toBe(false);
|
||||
|
||||
const { wrapper: hiddenWrapper } = mountToolbar({ context: contexts.Hidden });
|
||||
const hiddenActions = hiddenWrapper.vm.menuActions();
|
||||
const hiddenUpload = hiddenActions.find((a) => a.name === "upload");
|
||||
const hiddenDocs = hiddenActions.find((a) => a.name === "docs");
|
||||
const hiddenTroubleshooting = hiddenActions.find((a) => a.name === "troubleshooting");
|
||||
|
||||
expect(hiddenUpload).toBeDefined();
|
||||
expect(hiddenDocs).toBeDefined();
|
||||
expect(hiddenTroubleshooting).toBeDefined();
|
||||
|
||||
expect(hiddenUpload.visible).toBe(false);
|
||||
expect(hiddenDocs.visible).toBe(false);
|
||||
expect(hiddenTroubleshooting.visible).toBe(true);
|
||||
});
|
||||
|
||||
it("invokes refresh prop and publishes upload dialog events on click", () => {
|
||||
const refresh = vi.fn();
|
||||
const publish = vi.fn();
|
||||
const { wrapper } = mountToolbar({ refresh, eventPublish: publish });
|
||||
|
||||
const actions = wrapper.vm.menuActions();
|
||||
const refreshAction = actions.find((a) => a.name === "refresh");
|
||||
const uploadAction = actions.find((a) => a.name === "upload");
|
||||
|
||||
expect(refreshAction).toBeDefined();
|
||||
expect(uploadAction).toBeDefined();
|
||||
|
||||
refreshAction.click();
|
||||
expect(refresh).toHaveBeenCalledTimes(1);
|
||||
|
||||
uploadAction.click();
|
||||
expect(publish).toHaveBeenCalledWith("dialog.upload");
|
||||
});
|
||||
});
|
||||
|
||||
describe("view handling", () => {
|
||||
it("setView keeps list when listView search setting is enabled", () => {
|
||||
const refresh = vi.fn();
|
||||
const { wrapper } = mountToolbar({
|
||||
refresh,
|
||||
searchOverrides: { listView: true },
|
||||
});
|
||||
|
||||
wrapper.vm.expanded = true;
|
||||
wrapper.vm.setView("list");
|
||||
|
||||
expect(refresh).toHaveBeenCalledWith({ view: "list" });
|
||||
expect(wrapper.vm.expanded).toBe(false);
|
||||
});
|
||||
|
||||
it("setView falls back to mosaic when list view is disabled", () => {
|
||||
const refresh = vi.fn();
|
||||
const { wrapper } = mountToolbar({
|
||||
refresh,
|
||||
searchOverrides: { listView: false },
|
||||
});
|
||||
|
||||
wrapper.vm.expanded = true;
|
||||
wrapper.vm.setView("list");
|
||||
|
||||
expect(refresh).toHaveBeenCalledWith({ view: "mosaic" });
|
||||
expect(wrapper.vm.expanded).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sortOptions", () => {
|
||||
it("provides archive-specific sort options for archive context", () => {
|
||||
const { wrapper } = mountToolbar({ context: contexts.Archive });
|
||||
|
||||
const values = wrapper.vm.sortOptions.map((o) => o.value);
|
||||
expect(values).toContain("archived");
|
||||
expect(values).not.toContain("similar");
|
||||
expect(values).not.toContain("relevance");
|
||||
});
|
||||
|
||||
it("includes similarity and relevance options in default photos context", () => {
|
||||
const { wrapper } = mountToolbar({ context: contexts.Photos });
|
||||
|
||||
const values = wrapper.vm.sortOptions.map((o) => o.value);
|
||||
expect(values).toContain("similar");
|
||||
expect(values).toContain("relevance");
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete actions", () => {
|
||||
it("deleteAll opens confirmation dialog only when delete is allowed", () => {
|
||||
const allowAll = vi.fn(() => true);
|
||||
const { wrapper } = mountToolbar({
|
||||
allowMock: allowAll,
|
||||
featuresOverrides: { delete: true },
|
||||
});
|
||||
|
||||
wrapper.vm.deleteAll();
|
||||
expect(wrapper.vm.dialog.delete).toBe(true);
|
||||
|
||||
const denyDelete = vi.fn((resource, action) => {
|
||||
if (resource === "photos" && action === "delete") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const { wrapper: noDeleteWrapper } = mountToolbar({
|
||||
allowMock: denyDelete,
|
||||
featuresOverrides: { delete: true },
|
||||
});
|
||||
|
||||
noDeleteWrapper.vm.deleteAll();
|
||||
expect(noDeleteWrapper.vm.dialog.delete).toBe(false);
|
||||
});
|
||||
|
||||
it("batchDelete posts delete request and clears clipboard on success", async () => {
|
||||
const clipboard = { clear: vi.fn() };
|
||||
const { default: $notify } = await import("common/notify");
|
||||
const { default: $api } = await import("common/api");
|
||||
|
||||
const postSpy = vi.spyOn($api, "post").mockResolvedValue({ data: {} });
|
||||
const notifySpy = vi.spyOn($notify, "success");
|
||||
|
||||
const { wrapper } = mountToolbar({
|
||||
clipboard,
|
||||
featuresOverrides: { delete: true },
|
||||
});
|
||||
|
||||
wrapper.vm.dialog.delete = true;
|
||||
|
||||
await wrapper.vm.batchDelete();
|
||||
|
||||
expect(postSpy).toHaveBeenCalledWith("batch/photos/delete", { all: true });
|
||||
expect(wrapper.vm.dialog.delete).toBe(false);
|
||||
expect(notifySpy).toHaveBeenCalledWith("Permanently deleted");
|
||||
expect(clipboard.clear).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("browse actions", () => {
|
||||
it("clearLocation navigates back to browse list", () => {
|
||||
const push = vi.fn();
|
||||
const { wrapper } = mountToolbar({
|
||||
routerOverrides: {
|
||||
push,
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm.clearLocation();
|
||||
expect(push).toHaveBeenCalledWith({ name: "browse" });
|
||||
});
|
||||
|
||||
it("onBrowse opens places browse in new tab on desktop", () => {
|
||||
const push = vi.fn();
|
||||
const openUrlSpy = vi.fn();
|
||||
|
||||
const staticFilter = { q: "country:US" };
|
||||
const { wrapper, router, openUrl } = mountToolbar({
|
||||
staticFilter,
|
||||
routerOverrides: {
|
||||
push,
|
||||
resolve: vi.fn((route) => ({
|
||||
href: `/library/${route.name}?q=${route.query?.q || ""}`,
|
||||
})),
|
||||
},
|
||||
openUrlSpy,
|
||||
});
|
||||
|
||||
wrapper.vm.onBrowse();
|
||||
|
||||
expect(push).not.toHaveBeenCalled();
|
||||
expect(router.resolve).toHaveBeenCalledWith({ name: "places_browse", query: staticFilter });
|
||||
expect(openUrl).toHaveBeenCalledWith("/library/places_browse?q=country:US");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue