mirror of
https://github.com/photoprism/photoprism.git
synced 2026-01-23 02:24:24 +00:00
Tests: Improve test isolation and cleanup in vitest suites (#5387)
* Tests: Improve test isolation and cleanup in vitest suites * Tests: Add component tests for navigation, photo toolbar, and file editing * Tests: Remove unused notification spies from batch edit component tests * Tests: update Vitest `$notify` mock to use `vi.fn()` and include `info` method.
This commit is contained in:
parent
1b09c2b8e3
commit
cdf00beda4
14 changed files with 1377 additions and 79 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import "../fixtures";
|
||||
import Config from "common/config";
|
||||
import StorageShim from "node-storage-shim";
|
||||
|
|
@ -11,7 +11,31 @@ const createTestConfig = () => {
|
|||
return new Config(new StorageShim(), values);
|
||||
};
|
||||
|
||||
const resetThemesToDefault = () => {
|
||||
themes.SetOptions([
|
||||
{
|
||||
text: "Default",
|
||||
value: "default",
|
||||
disabled: false,
|
||||
},
|
||||
]);
|
||||
|
||||
themes.Set("default", {
|
||||
name: "default",
|
||||
title: "Default",
|
||||
colors: {},
|
||||
variables: {},
|
||||
});
|
||||
};
|
||||
|
||||
describe("common/config", () => {
|
||||
beforeEach(() => {
|
||||
resetThemesToDefault();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetThemesToDefault();
|
||||
});
|
||||
it("should get all config values", () => {
|
||||
const storage = new StorageShim();
|
||||
const values = { siteTitle: "Foo", name: "testConfig", year: "2300" };
|
||||
|
|
@ -116,42 +140,12 @@ describe("common/config", () => {
|
|||
variables: {},
|
||||
};
|
||||
|
||||
themes.SetOptions([
|
||||
{
|
||||
text: "Default",
|
||||
value: "default",
|
||||
disabled: false,
|
||||
},
|
||||
]);
|
||||
|
||||
themes.Set("default", {
|
||||
name: "default",
|
||||
title: "Default",
|
||||
colors: {},
|
||||
variables: {},
|
||||
});
|
||||
|
||||
themes.Assign([forcedTheme]);
|
||||
|
||||
cfg.setTheme("default");
|
||||
|
||||
expect(cfg.themeName).toBe("portal-forced");
|
||||
expect(cfg.theme.colors.background).toBe("#111111");
|
||||
|
||||
themes.Remove("portal-forced");
|
||||
themes.SetOptions([
|
||||
{
|
||||
text: "Default",
|
||||
value: "default",
|
||||
disabled: false,
|
||||
},
|
||||
]);
|
||||
themes.Set("default", {
|
||||
name: "default",
|
||||
title: "Default",
|
||||
colors: {},
|
||||
variables: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("should return app edition", () => {
|
||||
|
|
|
|||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { shallowMount, config as VTUConfig } from "@vue/test-utils";
|
||||
import { nextTick } from "vue";
|
||||
import PPhotoBatchEdit from "component/photo/batch-edit.vue";
|
||||
import * as contexts from "options/contexts";
|
||||
|
|
@ -146,20 +146,9 @@ describe("component/photo/batch-edit", () => {
|
|||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$notify: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
$lightbox: {
|
||||
openView: vi.fn(),
|
||||
},
|
||||
$event: {
|
||||
subscribe: vi.fn(),
|
||||
unsubscribe: vi.fn(),
|
||||
},
|
||||
$config: {
|
||||
feature: vi.fn().mockReturnValue(true),
|
||||
},
|
||||
$vuetify: { display: { mdAndDown: false } },
|
||||
},
|
||||
stubs: {
|
||||
|
|
@ -202,6 +191,7 @@ describe("component/photo/batch-edit", () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
if (wrapper) {
|
||||
wrapper.unmount();
|
||||
}
|
||||
|
|
@ -395,8 +385,6 @@ describe("component/photo/batch-edit", () => {
|
|||
expect(ctx.allowEdit).toBe(false);
|
||||
expect(ctx.allowSelect).toBe(false);
|
||||
expect(ctx.context).toBe(contexts.BatchEdit);
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it("should clamp invalid index to first photo", () => {
|
||||
|
|
@ -407,8 +395,6 @@ describe("component/photo/batch-edit", () => {
|
|||
|
||||
expect(ctx.index).toBe(0);
|
||||
expect(ctx.allowSelect).toBe(false);
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -1,8 +1,18 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import "../fixtures";
|
||||
import { Album, BatchSize } from "model/album";
|
||||
|
||||
describe("model/album", () => {
|
||||
let originalBatchSize;
|
||||
|
||||
beforeEach(() => {
|
||||
originalBatchSize = Album.batchSize();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Album.setBatchSize(originalBatchSize);
|
||||
});
|
||||
|
||||
it("should get route view", () => {
|
||||
const values = { ID: 5, Title: "Christmas 2019", Slug: "christmas-2019" };
|
||||
const album = new Album(values);
|
||||
|
|
@ -312,7 +322,6 @@ describe("model/album", () => {
|
|||
expect(Album.batchSize()).toBe(BatchSize);
|
||||
Album.setBatchSize(30);
|
||||
expect(Album.batchSize()).toBe(30);
|
||||
Album.setBatchSize(BatchSize);
|
||||
});
|
||||
|
||||
it("should like album", () => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,18 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import "../fixtures";
|
||||
import { Face, BatchSize } from "model/face";
|
||||
|
||||
describe("model/face", () => {
|
||||
let originalBatchSize;
|
||||
|
||||
beforeEach(() => {
|
||||
originalBatchSize = Face.batchSize();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Face.setBatchSize(originalBatchSize);
|
||||
});
|
||||
|
||||
it("should get face defaults", () => {
|
||||
const values = {};
|
||||
const face = new Face(values);
|
||||
|
|
@ -146,7 +156,6 @@ describe("model/face", () => {
|
|||
expect(Face.batchSize()).toBe(BatchSize);
|
||||
Face.setBatchSize(30);
|
||||
expect(Face.batchSize()).toBe(30);
|
||||
Face.setBatchSize(BatchSize);
|
||||
});
|
||||
|
||||
it("should get collection resource", () => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,18 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import "../fixtures";
|
||||
import { Label, BatchSize } from "model/label";
|
||||
|
||||
describe("model/label", () => {
|
||||
let originalBatchSize;
|
||||
|
||||
beforeEach(() => {
|
||||
originalBatchSize = Label.batchSize();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Label.setBatchSize(originalBatchSize);
|
||||
});
|
||||
|
||||
it("should get route view", () => {
|
||||
const values = { ID: 5, UID: "ABC123", Name: "Black Cat", Slug: "black-cat" };
|
||||
const label = new Label(values);
|
||||
|
|
@ -15,7 +25,6 @@ describe("model/label", () => {
|
|||
expect(Label.batchSize()).toBe(BatchSize);
|
||||
Label.setBatchSize(30);
|
||||
expect(Label.batchSize()).toBe(30);
|
||||
Label.setBatchSize(BatchSize);
|
||||
});
|
||||
|
||||
it("should return classes", () => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,18 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import "../fixtures";
|
||||
import { Marker, BatchSize } from "model/marker";
|
||||
|
||||
describe("model/marker", () => {
|
||||
let originalBatchSize;
|
||||
|
||||
beforeEach(() => {
|
||||
originalBatchSize = Marker.batchSize();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Marker.setBatchSize(originalBatchSize);
|
||||
});
|
||||
|
||||
it("should get marker defaults", () => {
|
||||
const values = { FileUID: "fghjojp" };
|
||||
const marker = new Marker(values);
|
||||
|
|
@ -193,7 +203,6 @@ describe("model/marker", () => {
|
|||
expect(Marker.batchSize()).toBe(BatchSize);
|
||||
Marker.setBatchSize(30);
|
||||
expect(Marker.batchSize()).toBe(30);
|
||||
Marker.setBatchSize(BatchSize);
|
||||
});
|
||||
|
||||
it("should get collection resource", () => {
|
||||
|
|
|
|||
|
|
@ -344,21 +344,21 @@ describe("model/photo", () => {
|
|||
expect(result5).toBe("July 2012");
|
||||
});
|
||||
|
||||
it("should test whether photo has location", () => {
|
||||
it("should report hasLocation true for non-zero coordinates", () => {
|
||||
const values = { ID: 5, Title: "Crazy Cat", Lat: 36.442881666666665, Lng: 28.229493333333334 };
|
||||
const photo = new Photo(values);
|
||||
const result = photo.hasLocation();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should test whether photo has location", () => {
|
||||
it("should report hasLocation false for zero coordinates", () => {
|
||||
const values = { ID: 5, Title: "Crazy Cat", Lat: 0, Lng: 0 };
|
||||
const photo = new Photo(values);
|
||||
const result = photo.hasLocation();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should get location", () => {
|
||||
it("should get primary location label with country", () => {
|
||||
const values = {
|
||||
ID: 5,
|
||||
Title: "Crazy Cat",
|
||||
|
|
@ -372,7 +372,7 @@ describe("model/photo", () => {
|
|||
expect(result).toBe("Cape Point, South Africa");
|
||||
});
|
||||
|
||||
it("should get location", () => {
|
||||
it("should get full location with state and country", () => {
|
||||
const values = {
|
||||
ID: 5,
|
||||
Title: "Crazy Cat",
|
||||
|
|
@ -389,7 +389,7 @@ describe("model/photo", () => {
|
|||
expect(result).toBe("Cape Point, State, South Africa");
|
||||
});
|
||||
|
||||
it("should get location", () => {
|
||||
it("should return Unknown when country name does not match", () => {
|
||||
const values = {
|
||||
ID: 5,
|
||||
Title: "Crazy Cat",
|
||||
|
|
@ -405,14 +405,14 @@ describe("model/photo", () => {
|
|||
expect(result).toBe("Unknown");
|
||||
});
|
||||
|
||||
it("should get location", () => {
|
||||
it("should return Unknown when only country name is set", () => {
|
||||
const values = { ID: 5, Title: "Crazy Cat", CountryName: "Africa", PlaceCity: "Cape Town" };
|
||||
const photo = new Photo(values);
|
||||
const result = photo.locationInfo();
|
||||
expect(result).toBe("Unknown");
|
||||
});
|
||||
|
||||
it("should get camera", () => {
|
||||
it("should get camera from model and file camera data", () => {
|
||||
const values = { ID: 5, Title: "Crazy Cat", CameraModel: "EOSD10", CameraMake: "Canon" };
|
||||
const photo = new Photo(values);
|
||||
const result = photo.getCamera();
|
||||
|
|
@ -438,7 +438,7 @@ describe("model/photo", () => {
|
|||
expect(photo2.getCamera()).toBe("Canon abc");
|
||||
});
|
||||
|
||||
it("should get camera", () => {
|
||||
it("should return Unknown when camera info is missing", () => {
|
||||
const values = { ID: 5, Title: "Crazy Cat" };
|
||||
const photo = new Photo(values);
|
||||
const result = photo.getCamera();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,18 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import "../fixtures";
|
||||
import { Subject, BatchSize } from "model/subject";
|
||||
|
||||
describe("model/subject", () => {
|
||||
let originalBatchSize;
|
||||
|
||||
beforeEach(() => {
|
||||
originalBatchSize = Subject.batchSize();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Subject.setBatchSize(originalBatchSize);
|
||||
});
|
||||
|
||||
it("should get face defaults", () => {
|
||||
const values = {};
|
||||
const subject = new Subject(values);
|
||||
|
|
@ -238,7 +248,6 @@ describe("model/subject", () => {
|
|||
expect(Subject.batchSize()).toBe(BatchSize);
|
||||
Subject.setBatchSize(30);
|
||||
expect(Subject.batchSize()).toBe(30);
|
||||
Subject.setBatchSize(BatchSize);
|
||||
});
|
||||
|
||||
it("should get collection resource", () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import "../fixtures";
|
||||
import * as options from "options/options";
|
||||
import {
|
||||
|
|
@ -25,6 +25,15 @@ import {
|
|||
} from "options/options";
|
||||
|
||||
describe("options/options", () => {
|
||||
let originalDefaultLocale;
|
||||
|
||||
beforeEach(() => {
|
||||
originalDefaultLocale = options.DefaultLocale;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
SetDefaultLocale(originalDefaultLocale);
|
||||
});
|
||||
it("should get timezones", () => {
|
||||
const timezones = options.TimeZones();
|
||||
expect(timezones[0].ID).toBe("Local");
|
||||
|
|
@ -93,13 +102,10 @@ describe("options/options", () => {
|
|||
});
|
||||
|
||||
it("should set default locale", () => {
|
||||
// Assuming DefaultLocale is exported and mutable for testing purposes
|
||||
// Initial state check might depend on test execution order, so we control it here.
|
||||
SetDefaultLocale("en"); // Ensure starting state
|
||||
SetDefaultLocale("en");
|
||||
expect(options.DefaultLocale).toBe("en");
|
||||
SetDefaultLocale("de");
|
||||
expect(options.DefaultLocale).toBe("de");
|
||||
SetDefaultLocale("en"); // Reset for other tests
|
||||
});
|
||||
|
||||
it("should return default when no locale is provided", () => {
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ if (typeof global.ResizeObserver === "undefined") {
|
|||
constructor(callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
observe() { }
|
||||
unobserve() { }
|
||||
disconnect() { }
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -54,22 +54,22 @@ config.global.mocks = {
|
|||
$event: {
|
||||
subscribe: () => "sub-id",
|
||||
subscribeOnce: () => "sub-id-once",
|
||||
unsubscribe: () => {},
|
||||
publish: () => {},
|
||||
unsubscribe: () => { },
|
||||
publish: () => { },
|
||||
},
|
||||
$view: {
|
||||
enter: () => {},
|
||||
leave: () => {},
|
||||
enter: () => { },
|
||||
leave: () => { },
|
||||
isActive: () => true,
|
||||
},
|
||||
$notify: { success: () => {}, error: () => {}, warn: () => {} },
|
||||
$notify: { success: vi.fn(), error: vi.fn(), warn: vi.fn(), info: vi.fn() },
|
||||
$fullscreen: {
|
||||
isSupported: () => true,
|
||||
isEnabled: () => false,
|
||||
request: () => Promise.resolve(),
|
||||
exit: () => Promise.resolve(),
|
||||
},
|
||||
$clipboard: { selection: [], has: () => false, toggle: () => {} },
|
||||
$clipboard: { selection: [], has: () => false, toggle: () => { } },
|
||||
$util: {
|
||||
hasTouch: () => false,
|
||||
encodeHTML: (s) => s,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue