From bced52c3f469d6f6b889182dbb453b25bba930c2 Mon Sep 17 00:00:00 2001 From: Olga Bulat Date: Fri, 9 Jun 2023 18:51:59 +0300 Subject: [PATCH 1/3] Add SELECT_SEARCH_RESULT event --- .../VAudioDetails/VRelatedAudio.vue | 26 +++++++++++++++++-- .../VSearchResultsGrid/VAudioCell.vue | 21 +++++++++++++++ .../VSearchResultsGrid/VImageCell.vue | 22 ++++++++++++++++ .../VSearchResultsGrid/VImageGrid.vue | 8 +++++- frontend/src/types/analytics.ts | 19 ++++++++++++++ 5 files changed, 93 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/VAudioDetails/VRelatedAudio.vue b/frontend/src/components/VAudioDetails/VRelatedAudio.vue index 319e1a262d8..bd248651406 100644 --- a/frontend/src/components/VAudioDetails/VRelatedAudio.vue +++ b/frontend/src/components/VAudioDetails/VRelatedAudio.vue @@ -10,7 +10,12 @@ class="-mx-2 mb-12 flex flex-col gap-4 md:-mx-4" >
  • - +
  • { return uiStore.isBreakpoint("md") ? "l" : "s" }) - return { audioTrackSize } + const { sendCustomEvent } = useAnalytics() + const sendSelectSearchResultEvent = (audio: AudioDetail) => { + sendCustomEvent("SELECT_SEARCH_RESULT", { + id: audio.id, + relatedTo: relatedMediaStore.mainMediaId, + mediaType: AUDIO, + provider: audio.provider, + query: useSearchStore().searchTerm, + }) + } + + return { audioTrackSize, sendSelectSearchResultEvent } }, }) diff --git a/frontend/src/components/VSearchResultsGrid/VAudioCell.vue b/frontend/src/components/VSearchResultsGrid/VAudioCell.vue index 0659fad3c16..74bba4ca685 100644 --- a/frontend/src/components/VSearchResultsGrid/VAudioCell.vue +++ b/frontend/src/components/VSearchResultsGrid/VAudioCell.vue @@ -6,6 +6,7 @@ :search-term="searchTerm" v-bind="$attrs" v-on="$listeners" + @mousedown="sendSelectSearchResultEvent(audio)" /> @@ -15,6 +16,9 @@ import { defineComponent, PropType } from "vue" import type { AudioDetail } from "~/types/media" +import { useAnalytics } from "~/composables/use-analytics" +import { AUDIO } from "~/constants/media" + import VAudioTrack from "~/components/VAudioTrack/VAudioTrack.vue" export default defineComponent({ @@ -30,5 +34,22 @@ export default defineComponent({ required: true, }, }, + setup(props) { + const { sendCustomEvent } = useAnalytics() + + const sendSelectSearchResultEvent = (audio: AudioDetail) => { + sendCustomEvent("SELECT_SEARCH_RESULT", { + id: audio.id, + mediaType: AUDIO, + query: props.searchTerm, + provider: audio.provider, + relatedTo: null, + }) + } + + return { + sendSelectSearchResultEvent, + } + }, }) diff --git a/frontend/src/components/VSearchResultsGrid/VImageCell.vue b/frontend/src/components/VSearchResultsGrid/VImageCell.vue index 2d8ef6ca0fa..f7d30d934a6 100644 --- a/frontend/src/components/VSearchResultsGrid/VImageCell.vue +++ b/frontend/src/components/VSearchResultsGrid/VImageCell.vue @@ -6,6 +6,7 @@ :href="imageLink" class="group relative block w-full overflow-hidden rounded-sm bg-dark-charcoal-10 text-dark-charcoal-10 focus-bold-filled" :aria-label="contextSensitiveTitle" + @mousedown="sendSelectSearchResultEvent" >
    , default: "square", }, + relatedTo: { + type: [String, null] as PropType, + default: null, + }, }, setup(props) { const isSquare = computed(() => props.aspectRatio === "square") @@ -139,6 +148,17 @@ export default defineComponent({ }) }) + const { sendCustomEvent } = useAnalytics() + const sendSelectSearchResultEvent = () => { + sendCustomEvent("SELECT_SEARCH_RESULT", { + id: props.image.id, + mediaType: IMAGE, + provider: props.image.provider, + query: props.searchTerm || "", + relatedTo: props.relatedTo, + }) + } + return { styles, imgWidth, @@ -152,6 +172,8 @@ export default defineComponent({ getImgDimension, isSquare, + + sendSelectSearchResultEvent, } }, }) diff --git a/frontend/src/components/VSearchResultsGrid/VImageGrid.vue b/frontend/src/components/VSearchResultsGrid/VImageGrid.vue index ef639dfa7ba..c9eb4181a0f 100644 --- a/frontend/src/components/VSearchResultsGrid/VImageGrid.vue +++ b/frontend/src/components/VSearchResultsGrid/VImageGrid.vue @@ -11,6 +11,7 @@ :image="image" :search-term="searchTerm" aspect-ratio="intrinsic" + :related-to="relatedTo" />
    @@ -33,6 +34,7 @@ import { computed, defineComponent, PropType } from "vue" import { useSearchStore } from "~/stores/search" +import { useRelatedMediaStore } from "~/stores/media/related-media" import type { FetchState } from "~/types/fetch-state" import type { ImageDetail } from "~/types/media" @@ -74,7 +76,11 @@ export default defineComponent({ const searchTerm = computed(() => searchStore.searchTerm) const isError = computed(() => Boolean(props.fetchState.fetchingError)) - return { isError, searchTerm } + const relatedTo = computed(() => { + return props.isSinglePage ? useRelatedMediaStore().mainMediaId : null + }) + + return { isError, searchTerm, relatedTo } }, }) diff --git a/frontend/src/types/analytics.ts b/frontend/src/types/analytics.ts index 596639f93d7..f5647e7073d 100644 --- a/frontend/src/types/analytics.ts +++ b/frontend/src/types/analytics.ts @@ -171,6 +171,25 @@ export type Events = { /** The slug of the license the user clicked on */ license: string } + /** + * Description: Whenever the user selects a result from the search results page. + * Questions: + * - Which results are most popular for given searches? + * - How often do searches lead to clicking a result? + * - Are there popular searches that do not result in result selection? + */ + SELECT_SEARCH_RESULT: { + /** The unique ID of the media */ + id: string + /** If the result is a related result, provide the ID of the 'original' result */ + relatedTo: string | null + /** The media type being searched */ + mediaType: SearchType + /** The slug (not the prettified name) of the provider */ + provider: string + /** The search term */ + query: string + } } /** From b34c7f6f3d6b0cdda934ec134ed623e1fe6462e7 Mon Sep 17 00:00:00 2001 From: Olga Bulat Date: Sat, 10 Jun 2023 08:42:30 +0300 Subject: [PATCH 2/3] Add unit tests --- frontend/test/unit/fixtures/image.js | 36 +++++++++++ .../VSearchResultsGrid/v-audio-cell.spec.js | 49 +++++++++++++++ .../VSearchResultsGrid/v-image-cell.spec.js | 49 +++++++++++++++ .../specs/components/v-related-audios.spec.js | 59 ++++++++++++++++--- .../specs/components/v-related-images.spec.js | 42 ++++++++++++- 5 files changed, 226 insertions(+), 9 deletions(-) create mode 100644 frontend/test/unit/fixtures/image.js create mode 100644 frontend/test/unit/specs/components/VSearchResultsGrid/v-audio-cell.spec.js create mode 100644 frontend/test/unit/specs/components/VSearchResultsGrid/v-image-cell.spec.js diff --git a/frontend/test/unit/fixtures/image.js b/frontend/test/unit/fixtures/image.js new file mode 100644 index 00000000000..2f1782bcc50 --- /dev/null +++ b/frontend/test/unit/fixtures/image.js @@ -0,0 +1,36 @@ +export const image = { + id: "f166c4a0-7207-4ea2-8728-15350a60d37f", + title: "Cat cafe in Seoul", + indexed_on: "2020-04-22T18:09:32.186574Z", + foreign_landing_url: "https://www.flickr.com/photos/36703170@N02/5060030894", + url: "https://live.staticflickr.com/4111/5060030894_96d2b21794_b.jpg", + creator: "toel-uru", + creator_url: "https://www.flickr.com/photos/36703170@N02", + license: "by-nc-sa", + license_version: "2.0", + license_url: "https://creativecommons.org/licenses/by-nc-sa/2.0/", + provider: "flickr", + source: "flickr", + category: null, + filesize: null, + filetype: null, + tags: [ + { + name: "cat", + accuracy: null, + }, + ], + attribution: + '"Cat cafe in Seoul" by toel-uru is licensed under CC BY-NC-SA 2.0. To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/2.0/.', + fields_matched: ["description", "tags.name", "title"], + mature: false, + height: 681, + width: 1024, + thumbnail: + "https://api.openverse.engineering/v1/images/f166c4a0-7207-4ea2-8728-15350a60d37f/thumb/", + detail_url: + "https://api.openverse.engineering/v1/images/f166c4a0-7207-4ea2-8728-15350a60d37f/", + related_url: + "https://api.openverse.engineering/v1/images/f166c4a0-7207-4ea2-8728-15350a60d37f/related/", + unstable__sensitivity: ["sensitive_text"], +} diff --git a/frontend/test/unit/specs/components/VSearchResultsGrid/v-audio-cell.spec.js b/frontend/test/unit/specs/components/VSearchResultsGrid/v-audio-cell.spec.js new file mode 100644 index 00000000000..ab6a55519ec --- /dev/null +++ b/frontend/test/unit/specs/components/VSearchResultsGrid/v-audio-cell.spec.js @@ -0,0 +1,49 @@ +import { fireEvent } from "@testing-library/vue" + +import { render } from "~~/test/unit/test-utils/render" + +import { getAudioObj } from "~~/test/unit/fixtures/audio" + +import { useAnalytics } from "~/composables/use-analytics" +import { AUDIO } from "~/constants/media" + +import VAudioCell from "~/components/VSearchResultsGrid/VAudioCell.vue" + +jest.mock("~/composables/use-analytics", () => ({ + useAnalytics: jest.fn(), +})) + +describe("VAudioCell", () => { + let options = {} + let sendCustomEventMock = null + const audio = getAudioObj() + + beforeEach(() => { + sendCustomEventMock = jest.fn() + useAnalytics.mockImplementation(() => ({ + sendCustomEvent: sendCustomEventMock, + })) + options = { + props: { + audio, + searchTerm: "cat", + relatedTo: null, + }, + } + }) + + it("sends SELECT_SEARCH_RESULT event when clicked", async () => { + const { getByRole } = render(VAudioCell, options) + const link = getByRole("application") + + await fireEvent.click(link) + + expect(sendCustomEventMock).toHaveBeenCalledWith("SELECT_SEARCH_RESULT", { + id: audio.id, + mediaType: AUDIO, + query: "cat", + provider: audio.provider, + relatedTo: null, + }) + }) +}) diff --git a/frontend/test/unit/specs/components/VSearchResultsGrid/v-image-cell.spec.js b/frontend/test/unit/specs/components/VSearchResultsGrid/v-image-cell.spec.js new file mode 100644 index 00000000000..331aae1c0dd --- /dev/null +++ b/frontend/test/unit/specs/components/VSearchResultsGrid/v-image-cell.spec.js @@ -0,0 +1,49 @@ +import { fireEvent } from "@testing-library/vue" + +import { render } from "~~/test/unit/test-utils/render" +import { image } from "~~/test/unit/fixtures/image" + +import { useAnalytics } from "~/composables/use-analytics" + +import { IMAGE } from "~/constants/media" + +import VImageCell from "~/components/VSearchResultsGrid/VImageCell.vue" + +jest.mock("~/composables/use-analytics", () => ({ + useAnalytics: jest.fn(), +})) + +describe("VImageCell", () => { + let options = {} + let sendCustomEventMock = null + + beforeEach(() => { + sendCustomEventMock = jest.fn() + useAnalytics.mockImplementation(() => ({ + sendCustomEvent: sendCustomEventMock, + })) + options = { + props: { + image, + searchTerm: "cat", + aspectRatio: "square", + relatedTo: null, + }, + } + }) + + it("sends SELECT_SEARCH_RESULT event when clicked", async () => { + const { getByRole } = render(VImageCell, options) + const link = getByRole("link") + + await fireEvent.click(link) + + expect(sendCustomEventMock).toHaveBeenCalledWith("SELECT_SEARCH_RESULT", { + id: image.id, + mediaType: IMAGE, + query: "cat", + provider: image.provider, + relatedTo: null, + }) + }) +}) diff --git a/frontend/test/unit/specs/components/v-related-audios.spec.js b/frontend/test/unit/specs/components/v-related-audios.spec.js index dd90555c200..680ebf76be3 100644 --- a/frontend/test/unit/specs/components/v-related-audios.spec.js +++ b/frontend/test/unit/specs/components/v-related-audios.spec.js @@ -1,21 +1,39 @@ -import { screen } from "@testing-library/vue" +import { fireEvent, screen } from "@testing-library/vue" import { render } from "~~/test/unit/test-utils/render" import { getAudioObj } from "~~/test/unit/fixtures/audio" +import { AUDIO } from "~/constants/media" +import { useAnalytics } from "~/composables/use-analytics" +import { useRelatedMediaStore } from "~/stores/media/related-media" +import { useSearchStore } from "~/stores/search" + import VRelatedAudio from "~/components/VAudioDetails/VRelatedAudio.vue" const audioResults = [getAudioObj(), getAudioObj()] +jest.mock("~/composables/use-analytics", () => ({ + useAnalytics: jest.fn(), +})) describe("RelatedAudios", () => { + let options = { + propsData: { + media: audioResults, + fetchState: { isFetching: false, isError: false }, + }, + stubs: { LoadingIcon: true, VAudioThumbnail: true }, + } + let sendCustomEventMock = null + + beforeEach(() => { + sendCustomEventMock = jest.fn() + useAnalytics.mockImplementation(() => ({ + sendCustomEvent: sendCustomEventMock, + })) + }) + it("should render content when finished loading related audios", async () => { - await render(VRelatedAudio, { - propsData: { - media: audioResults, - fetchState: { isFetching: false, isError: false }, - }, - stubs: { LoadingIcon: true, VAudioThumbnail: true }, - }) + render(VRelatedAudio, options) screen.getByText(/related audio/i) @@ -27,4 +45,29 @@ describe("RelatedAudios", () => { audioResults.length * 2 ) }) + + it("should send SELECT_SEARCH_RESULT event when clicked", async () => { + const mainMediaId = "123" + const query = "cat" + const { queryAllByRole } = render( + VRelatedAudio, + options, + (localVue, options) => { + const relatedMediaStore = useRelatedMediaStore(options.pinia) + relatedMediaStore.$patch({ mainMediaId }) + useSearchStore(localVue.pinia).$patch({ searchTerm: query }) + } + ) + const audioLink = queryAllByRole("application")[0] + + await fireEvent.click(audioLink) + + expect(sendCustomEventMock).toHaveBeenCalledWith("SELECT_SEARCH_RESULT", { + id: audioResults[0].id, + mediaType: AUDIO, + query, + provider: audioResults[0].provider, + relatedTo: mainMediaId, + }) + }) }) diff --git a/frontend/test/unit/specs/components/v-related-images.spec.js b/frontend/test/unit/specs/components/v-related-images.spec.js index 82f5e5f7660..b0635a5b47b 100644 --- a/frontend/test/unit/specs/components/v-related-images.spec.js +++ b/frontend/test/unit/specs/components/v-related-images.spec.js @@ -1,9 +1,18 @@ -import { screen } from "@testing-library/vue" +import { fireEvent, screen } from "@testing-library/vue" import { render } from "~~/test/unit/test-utils/render" +import { IMAGE } from "~/constants/media" +import { useSearchStore } from "~/stores/search" +import { useRelatedMediaStore } from "~/stores/media/related-media" +import { useAnalytics } from "~/composables/use-analytics" + import VRelatedImages from "~/components/VImageDetails/VRelatedImages.vue" +jest.mock("~/composables/use-analytics", () => ({ + useAnalytics: jest.fn(), +})) + const media = [ { id: "img1", url: "https://wp.org/img1.jpg" }, { id: "img2", url: "https://wp.org/img2.jpg" }, @@ -12,12 +21,17 @@ const media = [ describe("RelatedImage", () => { let props let options + let sendCustomEventMock = null beforeEach(() => { props = { media, fetchState: { isFetching: false } } options = { propsData: props, stubs: ["VLicense"], } + sendCustomEventMock = jest.fn() + useAnalytics.mockImplementation(() => ({ + sendCustomEvent: sendCustomEventMock, + })) }) it("should render an image grid", () => { render(VRelatedImages, options) @@ -36,4 +50,30 @@ describe("RelatedImage", () => { expect(screen.getByRole("heading").textContent).toContain("Related images") expect(screen.queryAllByRole("img").length).toEqual(0) }) + + it("should send SELECT_SEARCH_RESULT event when clicked", async () => { + const mainMediaId = "123" + const query = "cat" + + const { queryAllByRole } = render( + VRelatedImages, + options, + (localVue, options) => { + const relatedMediaStore = useRelatedMediaStore(options.pinia) + relatedMediaStore.$patch({ mainMediaId }) + useSearchStore(options.pinia).$patch({ searchTerm: query }) + } + ) + const imageLink = queryAllByRole("link")[0] + + await fireEvent.click(imageLink) + + expect(sendCustomEventMock).toHaveBeenCalledWith("SELECT_SEARCH_RESULT", { + id: media[0].id, + mediaType: IMAGE, + query, + provider: media[0].provider, + relatedTo: mainMediaId, + }) + }) }) From 68c0ff4600308a56b62696c2026984c8d89e9358 Mon Sep 17 00:00:00 2001 From: Olga Bulat Date: Fri, 16 Jun 2023 16:04:05 +0300 Subject: [PATCH 3/3] Update frontend/test/unit/specs/components/VSearchResultsGrid/v-image-cell.spec.js --- .../specs/components/VSearchResultsGrid/v-image-cell.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/test/unit/specs/components/VSearchResultsGrid/v-image-cell.spec.js b/frontend/test/unit/specs/components/VSearchResultsGrid/v-image-cell.spec.js index 331aae1c0dd..73fa7d414ab 100644 --- a/frontend/test/unit/specs/components/VSearchResultsGrid/v-image-cell.spec.js +++ b/frontend/test/unit/specs/components/VSearchResultsGrid/v-image-cell.spec.js @@ -26,7 +26,6 @@ describe("VImageCell", () => { props: { image, searchTerm: "cat", - aspectRatio: "square", relatedTo: null, }, }