mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-17 16:49:04 +00:00
Tests: Adapt tests to frontend changes and cover additional improvements
This commit is contained in:
parent
deb42b0e29
commit
c6428da761
5 changed files with 213 additions and 22 deletions
|
|
@ -299,6 +299,16 @@ test.meta("testID", "settings-general-004").meta({ type: "short", mode: "auth" }
|
|||
await t.expect(photoviewer.markersEditToggle.exists).ok();
|
||||
await photoviewer.triggerPhotoViewerAction("close-button");
|
||||
|
||||
await photo.toggleSelectNthPhoto(0, "image");
|
||||
await photo.toggleSelectNthPhoto(1, "image");
|
||||
await contextmenu.triggerContextMenuAction("edit");
|
||||
|
||||
await t.expect(photoedit.batchDialog.visible).ok();
|
||||
await t.expect(photoedit.batchLabels.visible).ok();
|
||||
|
||||
await t.click(photoedit.dialogClose);
|
||||
await contextmenu.clearSelection();
|
||||
|
||||
await menu.checkMenuItemAvailability("people", true);
|
||||
await menu.checkMenuItemAvailability("labels", true);
|
||||
await menu.openPage("settings");
|
||||
|
|
@ -327,6 +337,16 @@ test.meta("testID", "settings-general-004").meta({ type: "short", mode: "auth" }
|
|||
await t.expect(photoviewer.markersEditToggle.exists).notOk();
|
||||
await photoviewer.triggerPhotoViewerAction("close-button");
|
||||
|
||||
await photo.toggleSelectNthPhoto(0, "image");
|
||||
await photo.toggleSelectNthPhoto(1, "image");
|
||||
await contextmenu.triggerContextMenuAction("edit");
|
||||
|
||||
await t.expect(photoedit.batchDialog.visible).ok();
|
||||
await t.expect(photoedit.batchLabels.exists).notOk();
|
||||
|
||||
await t.click(photoedit.dialogClose);
|
||||
await contextmenu.clearSelection();
|
||||
|
||||
await menu.checkMenuItemAvailability("people", false);
|
||||
await menu.checkMenuItemAvailability("labels", false);
|
||||
|
||||
|
|
@ -557,7 +577,9 @@ test.meta("testID", "settings-general-006").meta({ type: "short", mode: "auth" }
|
|||
await photoviewer.checkPhotoViewerActionAvailability("edit-button", false);
|
||||
|
||||
await photoviewer.openSidebar();
|
||||
await photoviewer.assertSidebarIsReadOnly();
|
||||
// The first non-stacked photo has Title + Labels + Albums but no Caption or People;
|
||||
// the Filmpreis-anchored block below covers the People positive case.
|
||||
await photoviewer.assertSidebarIsReadOnly({ expectCaption: false, expectPeople: false });
|
||||
await photoviewer.triggerPhotoViewerAction("close-button");
|
||||
await t.expect(Selector("div.p-lightbox__pswp").visible).notOk();
|
||||
|
||||
|
|
|
|||
|
|
@ -199,9 +199,6 @@ test.meta("testID", "sharing-002").meta({ type: "short", mode: "auth" })("Multi-
|
|||
});
|
||||
|
||||
test.meta("testID", "sharing-003").meta({ type: "short", mode: "auth" })("Common: Lightbox sidebar shows only restricted metadata on share links", async (t) => {
|
||||
// Reuse the shared album exposed by sharing-002. The Vitest role x
|
||||
// field matrix already covers the per-role logic against a mocked
|
||||
// $session; this test pins the real anonymous path end-to-end.
|
||||
await t.useRole(Role.anonymous());
|
||||
await t.navigateTo("http://localhost:2343/s/jxoux5ub1e/british-columbia-canada");
|
||||
await t.expect(toolbar.toolbarSecondTitle.withText("British Columbia").visible).ok();
|
||||
|
|
@ -209,20 +206,13 @@ test.meta("testID", "sharing-003").meta({ type: "short", mode: "auth" })("Common
|
|||
await photoviewer.openPhotoViewer("nth", 0);
|
||||
await photoviewer.openSidebar();
|
||||
|
||||
// Allow-list: file info and taken-at rows render for anonymous
|
||||
// viewers so they can tell what they are looking at.
|
||||
await t.expect(photoviewer.sidebarRow("mdi-calendar").exists).ok();
|
||||
|
||||
// Deny-list: edit affordances, face-marker controls, and every
|
||||
// restricted section must be gone.
|
||||
await photoviewer.assertSidebarIsReadOnly();
|
||||
await t.expect(photoviewer.markersVisibilityToggle.exists).notOk();
|
||||
await t.expect(photoviewer.markersEditToggle.exists).notOk();
|
||||
// The visitor share-link photo carries a Title but no Caption.
|
||||
await photoviewer.assertSidebarIsReadOnly({ restricted: true, expectCaption: false });
|
||||
// Merged file row renders for restricted sessions (type + size as
|
||||
// the title) but the filename subtitle must be suppressed.
|
||||
await t.expect(Selector(".p-lightbox-sidebar .meta-file .v-list-item-subtitle").exists).notOk();
|
||||
await t.expect(photoviewer.sidebarRow("mdi-camera").exists).notOk();
|
||||
await t.expect(photoviewer.sidebarRow("mdi-camera-iris").exists).notOk();
|
||||
await t.expect(Selector(".p-lightbox-sidebar .text-subtitle-2").withText("People").exists).notOk();
|
||||
await t.expect(Selector(".p-lightbox-sidebar .text-subtitle-2").withText("Labels").exists).notOk();
|
||||
await t.expect(Selector(".p-lightbox-sidebar .text-subtitle-2").withText("Albums").exists).notOk();
|
||||
|
|
|
|||
|
|
@ -67,6 +67,72 @@ test.meta("testID", "sidebar-edit-002").meta({ mode: "public" })("Common: Adds a
|
|||
await t.expect(Selector("div.is-photo").withAttribute("data-uid", uid).exists).ok();
|
||||
});
|
||||
|
||||
test.meta("testID", "sidebar-edit-004").meta({ mode: "public" })(
|
||||
"Common: Removes a label and an album inline, undoes before save, and keeps them on the photo",
|
||||
async (t) => {
|
||||
const uid = await photoviewer.openSidebarOnFirstPhoto();
|
||||
|
||||
const stamp = Date.now();
|
||||
const labelTitle = `SidebarEditLabelUndo-${stamp}`;
|
||||
const albumTitle = `SidebarEditAlbumUndo-${stamp}`;
|
||||
|
||||
await photoviewer.typeAndConfirmInlineChip("Labels", labelTitle);
|
||||
await photoviewer.typeAndConfirmInlineChip("Albums", albumTitle);
|
||||
|
||||
const labelChip = photoviewer.chipByTitle("Labels", labelTitle);
|
||||
const albumChip = photoviewer.chipByTitle("Albums", albumTitle);
|
||||
|
||||
await photoviewer.removeInlineChip("Labels", labelTitle);
|
||||
await t.expect(labelChip.exists).notOk();
|
||||
await photoviewer.removeInlineChip("Albums", albumTitle);
|
||||
await t.expect(albumChip.exists).notOk();
|
||||
|
||||
await photoviewer.undoChipRemovals("Labels");
|
||||
await t.expect(labelChip.exists).ok();
|
||||
await photoviewer.undoChipRemovals("Albums");
|
||||
await t.expect(albumChip.exists).ok();
|
||||
|
||||
// Close + reopen rehydrates the sidebar from the API; both chips must
|
||||
// still be there because Undo never reached the backend.
|
||||
await photoviewer.triggerPhotoViewerAction("close-button");
|
||||
await photoviewer.openSidebarOnPhoto(uid);
|
||||
|
||||
await t.expect(photoviewer.chipByTitle("Labels", labelTitle).exists).ok();
|
||||
await t.expect(photoviewer.chipByTitle("Albums", albumTitle).exists).ok();
|
||||
}
|
||||
);
|
||||
|
||||
test.meta("testID", "sidebar-edit-005").meta({ mode: "public" })(
|
||||
"Common: Removes a label and an album inline, saves the removal, and detaches them from the photo",
|
||||
async (t) => {
|
||||
const uid = await photoviewer.openSidebarOnFirstPhoto();
|
||||
|
||||
const stamp = Date.now();
|
||||
const labelTitle = `SidebarEditLabelSave-${stamp}`;
|
||||
const albumTitle = `SidebarEditAlbumSave-${stamp}`;
|
||||
|
||||
await photoviewer.typeAndConfirmInlineChip("Labels", labelTitle);
|
||||
await photoviewer.typeAndConfirmInlineChip("Albums", albumTitle);
|
||||
|
||||
await photoviewer.removeInlineChip("Labels", labelTitle);
|
||||
await photoviewer.removeInlineChip("Albums", albumTitle);
|
||||
|
||||
await photoviewer.confirmChipRemovals("Labels");
|
||||
await photoviewer.confirmChipRemovals("Albums");
|
||||
|
||||
await t.expect(photoviewer.chipByTitle("Labels", labelTitle).exists).notOk();
|
||||
await t.expect(photoviewer.chipByTitle("Albums", albumTitle).exists).notOk();
|
||||
|
||||
// Close + reopen confirms the removal persisted past the round-trip;
|
||||
// the saved photo no longer references either chip.
|
||||
await photoviewer.triggerPhotoViewerAction("close-button");
|
||||
await photoviewer.openSidebarOnPhoto(uid);
|
||||
|
||||
await t.expect(photoviewer.chipByTitle("Labels", labelTitle).exists).notOk();
|
||||
await t.expect(photoviewer.chipByTitle("Albums", albumTitle).exists).notOk();
|
||||
}
|
||||
);
|
||||
|
||||
test.meta("testID", "sidebar-edit-003").meta({ mode: "public" })(
|
||||
"Common: Edits every taken-at, camera, and location field and confirms persistence",
|
||||
async (t) => {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export default class Page {
|
|||
this.batchDialogToolbarCloseAction = Selector("div.v-dialog--batch-edit header.v-toolbar button.action-close");
|
||||
this.batchToggleAllCheckbox = Selector("div.v-dialog--batch-edit .toggle-all div.v-selection-control__input");
|
||||
this.batchToggleSelectCheckbox = Selector("div.v-dialog--batch-edit .toggle-select div.v-selection-control__input");
|
||||
this.batchLabels = Selector("div.v-dialog--batch-edit .input-labels", { timeout: 15000 });
|
||||
|
||||
this.detailsDone = Selector(".p-form-photo-details-meta button.action-done", {
|
||||
timeout: 15000,
|
||||
|
|
|
|||
|
|
@ -107,10 +107,7 @@ export default class Page {
|
|||
// typeAndConfirmInlineChip adds a Labels or Albums chip via the always-rendered combobox.
|
||||
// The short wait lets Vuetify's combobox seat the typed value before Enter commits it.
|
||||
async typeAndConfirmInlineChip(sectionLabel, value) {
|
||||
const sectionClass = sectionLabel === "Labels" ? "meta-labels" : sectionLabel === "Albums" ? "meta-albums" : null;
|
||||
if (!sectionClass) {
|
||||
throw new Error(`Unknown chip section: ${sectionLabel}`);
|
||||
}
|
||||
const sectionClass = this._chipSectionClass(sectionLabel);
|
||||
const input = Selector(`.p-lightbox-sidebar .${sectionClass} .meta-inline-edit input`, { timeout: 15000 });
|
||||
await t.click(input).typeText(input, value);
|
||||
await t.wait(200);
|
||||
|
|
@ -118,6 +115,42 @@ export default class Page {
|
|||
await t.expect(Selector(".meta-inline-menu").exists).notOk();
|
||||
}
|
||||
|
||||
// chipByTitle returns the .meta-chip in the Labels or Albums section matching `title`.
|
||||
chipByTitle(sectionLabel, title) {
|
||||
const sectionClass = this._chipSectionClass(sectionLabel);
|
||||
return Selector(`.p-lightbox-sidebar .${sectionClass}.metadata__chips .meta-chip`, { timeout: 15000 }).withText(title);
|
||||
}
|
||||
|
||||
// removeInlineChip clicks the × on a Labels or Albums chip matching `title`.
|
||||
// Soft-removes locally via chipState.<field>.removals until confirm or undo.
|
||||
async removeInlineChip(sectionLabel, title) {
|
||||
await t.click(this.chipByTitle(sectionLabel, title).find(".meta-chip__remove"));
|
||||
}
|
||||
|
||||
// undoChipRemovals clicks Undo in the Labels or Albums chip toolbar,
|
||||
// restoring all soft-removed chips for that section.
|
||||
async undoChipRemovals(sectionLabel) {
|
||||
const sectionClass = this._chipSectionClass(sectionLabel);
|
||||
await t.click(Selector(`.p-lightbox-sidebar .${sectionClass} .meta-chip-undo`, { timeout: 15000 }));
|
||||
}
|
||||
|
||||
// confirmChipRemovals clicks Save in the Labels or Albums chip toolbar,
|
||||
// persisting all pending removals.
|
||||
async confirmChipRemovals(sectionLabel) {
|
||||
const sectionClass = this._chipSectionClass(sectionLabel);
|
||||
await t.click(Selector(`.p-lightbox-sidebar .${sectionClass} .meta-chip-confirm`, { timeout: 15000 }));
|
||||
}
|
||||
|
||||
_chipSectionClass(sectionLabel) {
|
||||
if (sectionLabel === "Labels") {
|
||||
return "meta-labels";
|
||||
}
|
||||
if (sectionLabel === "Albums") {
|
||||
return "meta-albums";
|
||||
}
|
||||
throw new Error(`Unknown chip section: ${sectionLabel}`);
|
||||
}
|
||||
|
||||
// startInlineEditOrAdd enters edit mode for Title or Caption by clicking the row, or
|
||||
// falls back to the add-prompt when the field is empty.
|
||||
async startInlineEditOrAdd(displayClass, promptLabel) {
|
||||
|
|
@ -164,6 +197,16 @@ export default class Page {
|
|||
// editor or dialog. Paired with assertSidebarIsReadOnly() so every selector has both a
|
||||
// positive and a negative assertion.
|
||||
async assertSidebarIsEditable() {
|
||||
// Title and Caption render unconditionally in editable mode; click each
|
||||
// to open its inline editor and escape back out before continuing.
|
||||
await this.startInlineEditOrAdd("meta-title", "Add a Title");
|
||||
await t.expect(Selector(".p-lightbox-sidebar .meta-inline-title").visible).ok();
|
||||
await t.pressKey("esc");
|
||||
|
||||
await this.startInlineEditOrAdd("meta-caption", "Add a Caption");
|
||||
await t.expect(Selector(".p-lightbox-sidebar .meta-inline-caption").visible).ok();
|
||||
await t.pressKey("esc");
|
||||
|
||||
await t.click(this.sidebarRow("mdi-calendar"));
|
||||
await t.expect(this.dateTimeDialog.root.visible).ok();
|
||||
await t.click(this.dateTimeDialog.cancel);
|
||||
|
|
@ -180,6 +223,18 @@ export default class Page {
|
|||
await t.click(this.locationDialog.cancel);
|
||||
}
|
||||
|
||||
// People, Albums, and Labels: assert their editable-only controls render.
|
||||
// The pencil toggle and chip-add inputs only exist when isEditable=true,
|
||||
// so they discriminate edit mode from read-only mode (which uses the eye
|
||||
// toggle and renders chips without an add input).
|
||||
await t.expect(this.peopleHeader.exists).ok();
|
||||
await t.expect(this.markersEditToggle.exists).ok();
|
||||
await t.expect(this.markersVisibilityToggle.exists).notOk();
|
||||
await t.expect(Selector(".p-lightbox-sidebar .v-list-item.meta-albums").exists).ok();
|
||||
await t.expect(Selector(".p-lightbox-sidebar .meta-albums .meta-inline-edit").exists).ok();
|
||||
await t.expect(Selector(".p-lightbox-sidebar .v-list-item.meta-labels").exists).ok();
|
||||
await t.expect(Selector(".p-lightbox-sidebar .meta-labels .meta-inline-edit").exists).ok();
|
||||
|
||||
// The next iteration's row click cancels the previous edit, so only the trailing
|
||||
// toolbar click is needed for cleanup. Gate on .visible so rows that v-show hides
|
||||
// for empty fields are skipped.
|
||||
|
|
@ -194,19 +249,76 @@ export default class Page {
|
|||
await t.click(Selector(".p-lightbox-sidebar .v-toolbar-title"));
|
||||
}
|
||||
|
||||
// assertSidebarIsReadOnly is the inverse of assertSidebarIsEditable.
|
||||
async assertSidebarIsReadOnly() {
|
||||
// assertSidebarIsReadOnly verifies the sidebar surfaces no editors or dialogs.
|
||||
// restricted=true asserts the visitor subset (Title, Caption, Date, Location).
|
||||
// expect* flags toggle positive existence checks; defaults assert presence.
|
||||
async assertSidebarIsReadOnly({
|
||||
restricted = false,
|
||||
expectTitle = true,
|
||||
expectCaption = true,
|
||||
expectPeople = true,
|
||||
expectLabels = true,
|
||||
expectAlbums = true,
|
||||
} = {}) {
|
||||
await t.click(this.sidebarRow("mdi-calendar"));
|
||||
await t.expect(this.dateTimeDialog.root.visible).notOk();
|
||||
|
||||
if (await this.sidebarRow("mdi-map-marker").exists) {
|
||||
await t.click(this.sidebarRow("mdi-map-marker"));
|
||||
await t.expect(this.locationDialog.root.visible).notOk();
|
||||
}
|
||||
|
||||
for (const [key, expected] of [
|
||||
["title", expectTitle],
|
||||
["caption", expectCaption],
|
||||
]) {
|
||||
const display = Selector(`.p-lightbox-sidebar .meta-${key}`);
|
||||
if (expected) {
|
||||
await t.expect(display.exists).ok();
|
||||
const row = display.parent(".v-list-item");
|
||||
await t.click(row);
|
||||
await t.expect(row.find(".meta-inline-edit").exists).notOk();
|
||||
} else {
|
||||
await t.expect(display.exists).notOk();
|
||||
}
|
||||
}
|
||||
// meta-add-prompt is an editing affordance and must never render here.
|
||||
await t.expect(Selector(".p-lightbox-sidebar .meta-add-prompt").visible).notOk();
|
||||
|
||||
if (restricted) {
|
||||
await t.expect(this.sidebarRow("mdi-camera").exists).notOk();
|
||||
await t.expect(this.sidebarRow("mdi-camera-iris").exists).notOk();
|
||||
await t.expect(this.peopleHeader.exists).notOk();
|
||||
await t.expect(Selector(".p-lightbox-sidebar .metadata__person-row").exists).notOk();
|
||||
await t.expect(this.markersVisibilityToggle.exists).notOk();
|
||||
await t.expect(this.markersEditToggle.exists).notOk();
|
||||
await t.expect(Selector(".p-lightbox-sidebar .meta-albums").exists).notOk();
|
||||
await t.expect(Selector(".p-lightbox-sidebar .meta-labels").exists).notOk();
|
||||
for (const key of ["subject", "artist", "copyright", "license", "keywords", "notes"]) {
|
||||
await t.expect(Selector(`.p-lightbox-sidebar .v-list-item.meta-${key}`).exists).notOk();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this.sidebarRow("mdi-camera").exists) {
|
||||
await t.click(this.sidebarRow("mdi-camera"));
|
||||
await t.expect(this.cameraDialog.root.visible).notOk();
|
||||
}
|
||||
|
||||
if (await this.sidebarRow("mdi-map-marker").exists) {
|
||||
await t.click(this.sidebarRow("mdi-map-marker"));
|
||||
await t.expect(this.locationDialog.root.visible).notOk();
|
||||
// Mirror the editable assertions: section renders, but with read-only
|
||||
// controls only — eye toggle for People, no chip-add input for Albums/Labels.
|
||||
if (expectPeople) {
|
||||
await t.expect(this.peopleHeader.exists).ok();
|
||||
await t.expect(this.markersVisibilityToggle.exists).ok();
|
||||
await t.expect(this.markersEditToggle.exists).notOk();
|
||||
}
|
||||
if (expectAlbums) {
|
||||
await t.expect(Selector(".p-lightbox-sidebar .v-list-item.meta-albums").exists).ok();
|
||||
await t.expect(Selector(".p-lightbox-sidebar .meta-albums .meta-inline-edit").exists).notOk();
|
||||
}
|
||||
if (expectLabels) {
|
||||
await t.expect(Selector(".p-lightbox-sidebar .v-list-item.meta-labels").exists).ok();
|
||||
await t.expect(Selector(".p-lightbox-sidebar .meta-labels .meta-inline-edit").exists).notOk();
|
||||
}
|
||||
|
||||
for (const key of ["subject", "artist", "copyright", "license", "keywords", "notes"]) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue