Tests: Adapt tests to frontend changes and cover additional improvements

This commit is contained in:
Theresa Gresch 2026-05-22 15:38:12 +02:00
parent deb42b0e29
commit c6428da761
5 changed files with 213 additions and 22 deletions

View file

@ -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();

View file

@ -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();

View file

@ -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) => {

View file

@ -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,

View file

@ -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"]) {