chore: add regression tests for tags (#33051)
(cherry picked from commit 7b7b5b8c5a1c13f65bb5696e7e06908b41a00e59)
This commit is contained in:
1 parent
12b2c6ac0a
commit
cca2dd7ece
6 files changed
+226
No files matched your search
@@ -0,0 +1,55 @@
|
||||
import { Route } from '#lib/route.ts';
|
||||
import { factory } from '#test/unit/factories.ts';
|
||||
import { test } from '#test/unit/fixtures.ts';
|
||||
import { expect } from 'vitest';
|
||||
import Page from './+page.svelte';
|
||||
import { load } from './+page.ts';
|
||||
|
||||
test('creating a tag under the current tag shows it as a child', async ({ data, router, render, settle, app, api }) => {
|
||||
data.addTags(factory.tag({ name: 'Travel' }));
|
||||
await router.goto(Route.tags({ path: 'Travel' }));
|
||||
await render(Page, load);
|
||||
await settle();
|
||||
|
||||
await app.tags.createButton.click();
|
||||
await app.tags.form.tagInput.fill('Japan');
|
||||
await app.tags.form.createButton.click();
|
||||
await settle();
|
||||
|
||||
expect(api.upsertTags).toHaveBeenCalledWith({ body: { tags: ['Travel/Japan'] } });
|
||||
await expect.element(app.tags.child('Japan')).toBeVisible();
|
||||
});
|
||||
|
||||
test('renaming the current tag opens its new path', async ({ data, router, render, settle, app, api }) => {
|
||||
const travel = factory.tag({ name: 'Travel' });
|
||||
data.addTags(travel);
|
||||
await router.goto(Route.tags({ path: 'Travel' }));
|
||||
await render(Page, load);
|
||||
await settle();
|
||||
|
||||
await app.tags.editButton.click();
|
||||
await app.tags.form.nameInput.fill('Trips');
|
||||
await app.tags.form.saveButton.click();
|
||||
await settle();
|
||||
|
||||
expect(api.updateTag).toHaveBeenCalledWith({ params: { id: travel.id }, body: { name: 'Trips', color: null } });
|
||||
expect(router.url.href).toBe(new URL(Route.tags({ path: 'Trips' }), router.url).href);
|
||||
await expect.element(app.tags.explorerLink('Trips')).toBeVisible();
|
||||
});
|
||||
|
||||
test('deleting the current tag opens its parent', async ({ data, router, render, settle, app, api }) => {
|
||||
const travel = factory.tag({ name: 'Travel' });
|
||||
const japan = factory.tag({ name: 'Japan', value: 'Travel/Japan', parentId: travel.id });
|
||||
data.addTags([travel, japan]);
|
||||
await router.goto(Route.tags({ path: 'Travel/Japan' }));
|
||||
await render(Page, load);
|
||||
await settle();
|
||||
|
||||
await app.tags.deleteButton.click();
|
||||
await app.tags.form.deleteButton.click();
|
||||
await settle();
|
||||
|
||||
expect(api.deleteTag).toHaveBeenCalledWith({ params: { id: japan.id } });
|
||||
expect(router.url.href).toBe(new URL(Route.tags({ path: 'Travel' }), router.url).href);
|
||||
await expect.element(app.tags.explorerLink('Japan')).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
type StackResponseDto,
|
||||
type StackUpdateDto,
|
||||
type TagResponseDto,
|
||||
type TagUpdateDto,
|
||||
type TimeBucketAssetResponseDto,
|
||||
type TimeBucketsResponseDto,
|
||||
type UpdateAlbumDto,
|
||||
@@ -506,6 +507,85 @@ export class MockData {
|
||||
return [...this.tags.values()];
|
||||
}
|
||||
|
||||
upsertTags(values: string[]) {
|
||||
return values.map((value) => this.upsertTag(value));
|
||||
}
|
||||
|
||||
updateTag(id: string, dto: TagUpdateDto) {
|
||||
const tag = this.tags.get(id);
|
||||
if (!tag) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = dto.name ?? tag.name;
|
||||
const value = tag.value.slice(0, tag.value.length - tag.name.length) + name;
|
||||
for (const descendant of this.tagList()) {
|
||||
if (descendant.value.startsWith(`${tag.value}/`)) {
|
||||
this.tags.set(descendant.id, { ...descendant, value: value + descendant.value.slice(tag.value.length) });
|
||||
}
|
||||
}
|
||||
|
||||
const updated = { ...tag, name, value, color: dto.color === undefined ? tag.color : (dto.color ?? undefined) };
|
||||
this.tags.set(id, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
deleteTag(id: string) {
|
||||
const tag = this.tags.get(id);
|
||||
if (!tag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const descendant of this.tagList()) {
|
||||
if (descendant.id === id || descendant.value.startsWith(`${tag.value}/`)) {
|
||||
this.tags.delete(descendant.id);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
tagAssets(assetIds: string[], tagIds: string[]) {
|
||||
const tags = tagIds.map((id) => this.tags.get(id)).filter((tag) => tag !== undefined);
|
||||
let count = 0;
|
||||
for (const assetId of assetIds) {
|
||||
const asset = this.assets.get(assetId);
|
||||
if (!asset) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = asset.tags ?? [];
|
||||
const added = tags.filter((tag) => existing.every(({ id }) => id !== tag.id));
|
||||
count += added.length;
|
||||
this.assets.set(assetId, { ...asset, tags: [...existing, ...added] });
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
untagAssets(tagId: string, assetIds: string[]) {
|
||||
return assetIds.map((id) => {
|
||||
const asset = this.assets.get(id);
|
||||
if (!asset?.tags?.some((tag) => tag.id === tagId)) {
|
||||
return { id, success: false };
|
||||
}
|
||||
|
||||
this.assets.set(id, { ...asset, tags: asset.tags.filter((tag) => tag.id !== tagId) });
|
||||
return { id, success: true };
|
||||
});
|
||||
}
|
||||
|
||||
private upsertTag(value: string): TagResponseDto {
|
||||
const existing = this.tagList().find((tag) => tag.value === value);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const separator = value.lastIndexOf('/');
|
||||
const parent = separator === -1 ? undefined : this.upsertTag(value.slice(0, separator));
|
||||
const tag = factory.tag({ name: value.slice(separator + 1), value, parentId: parent?.id });
|
||||
this.tags.set(tag.id, tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
addActivities(activities: MaybeArray<ActivityResponseDto>) {
|
||||
for (const activity of toList(activities)) {
|
||||
this.activities.set(activity.id, activity);
|
||||
|
||||
@@ -116,6 +116,14 @@ export const createHandlers = (data: MockData): Handlers => {
|
||||
const tag = data.getTag(params.id);
|
||||
return tag ? { json: tag } : notFound;
|
||||
},
|
||||
upsertTags: ({ body }) => ({ json: data.upsertTags(body.tags) }),
|
||||
updateTag: ({ params, body }) => {
|
||||
const tag = data.updateTag(params.id, body);
|
||||
return tag ? { json: tag } : notFound;
|
||||
},
|
||||
deleteTag: ({ params }) => (data.deleteTag(params.id) ? { status: 204 } : notFound),
|
||||
bulkTagAssets: ({ body }) => ({ json: { count: data.tagAssets(body.assetIds, body.tagIds) } }),
|
||||
untagAssets: ({ params, body }) => ({ json: data.untagAssets(params.id, body.ids) }),
|
||||
getSearchSuggestions: ({ params }) => ({ json: data.searchSuggestions(params) as string[] }),
|
||||
|
||||
updatePeople: ({ body }) => ({ json: data.updatePeople(body.people) }),
|
||||
|
||||
@@ -337,6 +337,17 @@ const createViewer = (page: Page) => {
|
||||
},
|
||||
navbarActions: page.getByTestId('asset-viewer-navbar-actions'),
|
||||
tags: page.getByTestId('detail-panel-tags').getByRole('link'),
|
||||
tag: (value: string) => page.getByTestId('detail-panel-tags').getByRole('link', { name: value, exact: true }),
|
||||
addTagButton: page.getByTestId('detail-panel-tags').getByRole('button', { name: 'add_tag', exact: true }),
|
||||
removeTagButton: page.getByTestId('detail-panel-tags').getByRole('button', { name: 'remove_tag', exact: true }),
|
||||
tagAssets: {
|
||||
input: page.getByRole('dialog').getByLabel('tag', { exact: true }),
|
||||
submitButton: page.getByRole('dialog').getByRole('button', { name: 'tag_assets', exact: true }),
|
||||
pick: async (value: string) => {
|
||||
await page.getByRole('dialog').getByLabel('tag', { exact: true }).fill(value);
|
||||
await page.getByRole('dialog').getByLabel('tag', { exact: true }).press('Enter');
|
||||
},
|
||||
},
|
||||
media,
|
||||
image: (quality: 'thumbnail' | 'preview' | 'original') => content.getByTestId(quality).filter({ visible: true }),
|
||||
stackThumbnail: (asset: Asset) => stack.locator(`[data-asset="${asset.id}"]`),
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Route } from '#lib/route.ts';
|
||||
import { factory } from '#test/common/factories.ts';
|
||||
import { expect, test } from '#test/medium/fixtures.ts';
|
||||
|
||||
test.describe('detail panel tags', () => {
|
||||
test.beforeEach(({ data }) => {
|
||||
data.setPreferences(factory.preferences({ tags: { enabled: true, sidebarWeb: false } }));
|
||||
});
|
||||
|
||||
test('tagging with an existing tag shows it in the panel', async ({ page, data, api, app }) => {
|
||||
const travel = factory.tag({ name: 'Travel' });
|
||||
const asset = factory.asset();
|
||||
data.addTags(travel).addAssets(asset);
|
||||
await page.goto(Route.viewAsset({ id: asset.id }));
|
||||
await app.viewer.waitForAsset(asset);
|
||||
await app.viewer.openDetailPanel();
|
||||
|
||||
await app.viewer.addTagButton.click();
|
||||
await app.viewer.tagAssets.pick('Travel');
|
||||
await app.viewer.tagAssets.submitButton.click();
|
||||
|
||||
await expect(app.viewer.tag('Travel')).toBeVisible();
|
||||
expect(api.upsertTags).not.toHaveBeenCalled();
|
||||
expect(api.bulkTagAssets).toHaveBeenCalledWith({ body: { assetIds: [asset.id], tagIds: [travel.id] } });
|
||||
});
|
||||
|
||||
test('tagging with a new tag creates it and shows it in the panel', async ({ page, data, api, app }) => {
|
||||
const asset = factory.asset();
|
||||
data.addAssets(asset);
|
||||
await page.goto(Route.viewAsset({ id: asset.id }));
|
||||
await app.viewer.waitForAsset(asset);
|
||||
await app.viewer.openDetailPanel();
|
||||
|
||||
await app.viewer.addTagButton.click();
|
||||
await app.viewer.tagAssets.pick('Japan');
|
||||
await app.viewer.tagAssets.submitButton.click();
|
||||
|
||||
await expect(app.viewer.tag('Japan')).toBeVisible();
|
||||
expect(api.upsertTags).toHaveBeenCalledWith({ body: { tags: ['Japan'] } });
|
||||
const [japan] = data.tagList();
|
||||
expect(api.bulkTagAssets).toHaveBeenCalledWith({ body: { assetIds: [asset.id], tagIds: [japan.id] } });
|
||||
});
|
||||
|
||||
test('removing a tag drops it from the panel', async ({ page, data, api, app }) => {
|
||||
const travel = factory.tag({ name: 'Travel' });
|
||||
const asset = factory.asset({ tags: [travel] });
|
||||
data.addTags(travel).addAssets(asset);
|
||||
await page.goto(Route.viewAsset({ id: asset.id }));
|
||||
await app.viewer.waitForAsset(asset);
|
||||
await app.viewer.openDetailPanel();
|
||||
await expect(app.viewer.tag('Travel')).toBeVisible();
|
||||
|
||||
await app.viewer.removeTagButton.click();
|
||||
|
||||
await expect(app.viewer.tag('Travel')).not.toBeVisible();
|
||||
expect(api.untagAssets).toHaveBeenCalledWith({ params: { id: travel.id }, body: { ids: [asset.id] } });
|
||||
});
|
||||
});
|
||||
@@ -112,6 +112,20 @@ export const createApp = () => {
|
||||
groupPosition: (current: number, total: number) => page.getByText(`${current} / ${total}`),
|
||||
emptyMessage: page.getByText('no_duplicates_found'),
|
||||
},
|
||||
tags: {
|
||||
createButton: button('create_tag'),
|
||||
editButton: button('edit_tag'),
|
||||
deleteButton: button('delete_tag'),
|
||||
explorerLink: (name: string) => sidebar.getByRole('link', { name, exact: true }),
|
||||
child: (name: string) => main.getByRole('button', { name, exact: true }),
|
||||
form: {
|
||||
tagInput: page.getByLabelText('tag', { exact: true }),
|
||||
nameInput: page.getByLabelText('name', { exact: true }),
|
||||
createButton: page.getByRole('dialog').getByRole('button', { name: 'create', exact: true }),
|
||||
saveButton: page.getByRole('dialog').getByRole('button', { name: 'save', exact: true }),
|
||||
deleteButton: page.getByRole('dialog').getByRole('button', { name: 'delete', exact: true }),
|
||||
},
|
||||
},
|
||||
trash: {
|
||||
emptyTrashButton: button('empty_trash'),
|
||||
restoreAllButton: button('restore_all'),
|
||||
|
||||
Reference in new issue
Block a user