From 83df85001d54e3ffaafea8e9a9a0842371e62c0f Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Fri, 13 Mar 2026 20:10:38 +0100 Subject: [PATCH] fix(webapp): fix search + search e2e (#9376) --- .github/workflows/test-e2e.yml | 6 + backend/src/db/factories.ts | 49 ++++ cypress/cypress.config.js | 2 +- cypress/e2e/Search.Results.feature | 67 +++++ .../I_click_on_the_next_page_button.js | 5 + .../I_click_on_the_{string}_tab.js | 6 + .../I_should_not_see_post_results.js | 5 + .../I_should_see_page_{string}.js | 7 + .../I_should_see_pagination_buttons.js | 6 + ...I_should_see_the_{string}_tab_as_active.js | 5 + .../I_should_see_{int}_group_results.js | 5 + .../I_should_see_{int}_hashtag_results.js | 5 + .../I_should_see_{int}_post_results.js | 5 + .../I_should_see_{int}_user_results.js | 5 + ..._following_{string}_are_in_the_database.js | 9 + .../features/SearchResults/SearchResults.vue | 241 ++++++++++-------- .../generic/TabNavigation/TabNavigation.vue | 2 +- webapp/graphql/Search.js | 4 +- webapp/pages/index.spec.js | 1 + webapp/pages/index.vue | 36 ++- .../_id/__snapshots__/_slug.spec.js.snap | 4 - 21 files changed, 348 insertions(+), 127 deletions(-) create mode 100644 cypress/e2e/Search.Results.feature create mode 100644 cypress/support/step_definitions/Search.Results/I_click_on_the_next_page_button.js create mode 100644 cypress/support/step_definitions/Search.Results/I_click_on_the_{string}_tab.js create mode 100644 cypress/support/step_definitions/Search.Results/I_should_not_see_post_results.js create mode 100644 cypress/support/step_definitions/Search.Results/I_should_see_page_{string}.js create mode 100644 cypress/support/step_definitions/Search.Results/I_should_see_pagination_buttons.js create mode 100644 cypress/support/step_definitions/Search.Results/I_should_see_the_{string}_tab_as_active.js create mode 100644 cypress/support/step_definitions/Search.Results/I_should_see_{int}_group_results.js create mode 100644 cypress/support/step_definitions/Search.Results/I_should_see_{int}_hashtag_results.js create mode 100644 cypress/support/step_definitions/Search.Results/I_should_see_{int}_post_results.js create mode 100644 cypress/support/step_definitions/Search.Results/I_should_see_{int}_user_results.js diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index a1d968a7e..b84a22bd2 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -233,6 +233,12 @@ jobs: timeout 120 bash -c 'until curl -sf http://localhost:3000 > /dev/null 2>&1; do sleep 5; done' echo "Webapp is ready." + - name: Initialize database + run: docker compose -f docker-compose.yml -f docker-compose.test.yml exec -T backend yarn db:migrate init + + - name: Migrate database + run: docker compose -f docker-compose.yml -f docker-compose.test.yml exec -T backend yarn db:migrate up + - name: Full stack tests | run tests id: e2e-tests run: yarn run cypress:run --spec "cypress/e2e/${{ matrix.feature }}" diff --git a/backend/src/db/factories.ts b/backend/src/db/factories.ts index d0ceae9ae..883898988 100644 --- a/backend/src/db/factories.ts +++ b/backend/src/db/factories.ts @@ -230,6 +230,55 @@ Factory.define('post') return post }) +Factory.define('group') + .option('ownerId', null) + .option('owner', ['ownerId'], (ownerId) => { + if (ownerId) return neode.find('User', ownerId) + return Factory.build('user') + }) + .attrs({ + id: uuid, + name: faker.company.name, + about: faker.lorem.sentence, + description: faker.lorem.paragraphs, + groupType: 'public', + actionRadius: 'regional', + deleted: false, + disabled: false, + }) + .attr('slug', ['slug', 'name'], (slug, name) => { + return slug || slugify(name, { lower: true }) + }) + .attr( + 'descriptionExcerpt', + ['descriptionExcerpt', 'description'], + (descriptionExcerpt, description) => { + return descriptionExcerpt || description + }, + ) + .after(async (buildObject, options) => { + const [group, owner] = await Promise.all([neode.create('Group', buildObject), options.owner]) + const session = driver.session() + try { + await session.writeTransaction((txc) => + txc.run( + ` + MATCH (owner:User {id: $ownerId}), (group:Group {id: $groupId}) + MERGE (owner)-[:CREATED]->(group) + MERGE (owner)-[membership:MEMBER_OF]->(group) + SET membership.createdAt = toString(datetime()), + membership.updatedAt = toString(datetime()), + membership.role = 'owner' + `, + { ownerId: owner.get('id'), groupId: buildObject.id }, + ), + ) + } finally { + await session.close() + } + return group + }) + Factory.define('comment') .option('postId', null) .option('post', ['postId'], (postId) => { diff --git a/cypress/cypress.config.js b/cypress/cypress.config.js index dafbe2933..78a661078 100644 --- a/cypress/cypress.config.js +++ b/cypress/cypress.config.js @@ -19,7 +19,7 @@ async function setupNodeEvents(on, config) { webpackPreprocessor({ webpackOptions: { mode: 'development', - devtool: 'source-map', + devtool: 'eval-source-map', resolve: { extensions: ['.js', '.json'], fallback: { diff --git a/cypress/e2e/Search.Results.feature b/cypress/e2e/Search.Results.feature new file mode 100644 index 000000000..5ebc0351a --- /dev/null +++ b/cypress/e2e/Search.Results.feature @@ -0,0 +1,67 @@ +Feature: Search Results Page + As a user + I would like to see search results for posts, users, groups, and hashtags + In order to find specific content on the platform + + Background: + Given the following "users" are in the database: + | slug | email | password | id | name | termsAndConditionsAgreedVersion | + | narrator | narrator@example.org | 1234 | narrator | Nathan Narrator | 0.0.4 | + | jenny | jenny@example.org | 1234 | jenny-id | Jenny Rostock | 0.0.4 | + | finduser | finduser@example.org | 1234 | finduser | Find me user | 0.0.4 | + And the following "tags" are in the database: + | id | + | find-me-tag | + And the following "posts" are in the database: + | id | title | content | authorId | tagIds | + | p1 | Find me post one | This is the first result | narrator | find-me-tag | + | p2 | Find me post two | This is the second result | narrator | find-me-tag | + And the following "groups" are in the database: + | id | name | slug | about | description | ownerId | + | group-1 | Discoverable club | discoverable-club | A group to be found | This is a detailed description for the test group so it has enough characters to pass the minimum length of one hundred | narrator | + And I am logged in as "narrator" + + Scenario: Post results are displayed + When I navigate to page "/search/search-results?search=Find" + Then I should see the "Post" tab as active + And I should see 2 post results + + Scenario: User results are displayed + When I navigate to page "/search/search-results?search=Jenny" + Then I should see the "User" tab as active + And I should see 1 user results + + Scenario: Group results are displayed + When I navigate to page "/search/search-results?search=Discoverable club" + Then I should see the "Group" tab as active + And I should see 1 group results + + Scenario: Hashtag results are displayed + When I navigate to page "/search/search-results?search=find-me-tag" + Then I should see the "Hashtag" tab as active + And I should see 1 hashtag results + + Scenario: Switching tabs hides previous results + When I navigate to page "/search/search-results?search=Find" + And I click on the "User" tab + Then I should not see post results + + Scenario: Pagination for many posts + Given the following "posts" are in the database: + | id | title | content | authorId | + | p3 | Find me post 3 | Some content 3 | narrator | + | p4 | Find me post 4 | Some content 4 | narrator | + | p5 | Find me post 5 | Some content 5 | narrator | + | p6 | Find me post 6 | Some content 6 | narrator | + | p7 | Find me post 7 | Some content 7 | narrator | + | p8 | Find me post 8 | Some content 8 | narrator | + | p9 | Find me post 9 | Some content 9 | narrator | + | p10 | Find me post 10 | Some content 10 | narrator | + | p11 | Find me post 11 | Some content 11 | narrator | + | p12 | Find me post 12 | Some content 12 | narrator | + | p13 | Find me post 13 | Some content 13 | narrator | + When I navigate to page "/search/search-results?search=Find" + Then I should see pagination buttons + And I should see page "Page 1 / 2" + When I click on the next page button + Then I should see page "Page 2 / 2" diff --git a/cypress/support/step_definitions/Search.Results/I_click_on_the_next_page_button.js b/cypress/support/step_definitions/Search.Results/I_click_on_the_next_page_button.js new file mode 100644 index 000000000..480da4fe5 --- /dev/null +++ b/cypress/support/step_definitions/Search.Results/I_click_on_the_next_page_button.js @@ -0,0 +1,5 @@ +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' + +defineStep('I click on the next page button', () => { + cy.get('[data-test="next-button"]').first().click() +}) diff --git a/cypress/support/step_definitions/Search.Results/I_click_on_the_{string}_tab.js b/cypress/support/step_definitions/Search.Results/I_click_on_the_{string}_tab.js new file mode 100644 index 000000000..cfa13f906 --- /dev/null +++ b/cypress/support/step_definitions/Search.Results/I_click_on_the_{string}_tab.js @@ -0,0 +1,6 @@ +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' + +defineStep('I click on the {string} tab', (type) => { + cy.get(`[data-test="${type}-tab"]`).should('not.have.class', '--disabled') + cy.get(`[data-test="${type}-tab-click"]`).click() +}) diff --git a/cypress/support/step_definitions/Search.Results/I_should_not_see_post_results.js b/cypress/support/step_definitions/Search.Results/I_should_not_see_post_results.js new file mode 100644 index 000000000..92ec9dc9d --- /dev/null +++ b/cypress/support/step_definitions/Search.Results/I_should_not_see_post_results.js @@ -0,0 +1,5 @@ +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' + +defineStep('I should not see post results', () => { + cy.get('.post-teaser').should('not.exist') +}) diff --git a/cypress/support/step_definitions/Search.Results/I_should_see_page_{string}.js b/cypress/support/step_definitions/Search.Results/I_should_see_page_{string}.js new file mode 100644 index 000000000..030cad39a --- /dev/null +++ b/cypress/support/step_definitions/Search.Results/I_should_see_page_{string}.js @@ -0,0 +1,7 @@ +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' + +defineStep('I should see page {string}', (page) => { + cy.get('.pagination-pageCount').first().invoke('text').then((text) => { + expect(text.replace(/\s+/g, ' ').trim()).to.contain(page) + }) +}) diff --git a/cypress/support/step_definitions/Search.Results/I_should_see_pagination_buttons.js b/cypress/support/step_definitions/Search.Results/I_should_see_pagination_buttons.js new file mode 100644 index 000000000..59da9a8c5 --- /dev/null +++ b/cypress/support/step_definitions/Search.Results/I_should_see_pagination_buttons.js @@ -0,0 +1,6 @@ +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' + +defineStep('I should see pagination buttons', () => { + cy.get('[data-test="previous-button"]').should('exist') + cy.get('[data-test="next-button"]').should('exist') +}) diff --git a/cypress/support/step_definitions/Search.Results/I_should_see_the_{string}_tab_as_active.js b/cypress/support/step_definitions/Search.Results/I_should_see_the_{string}_tab_as_active.js new file mode 100644 index 000000000..5e0387cda --- /dev/null +++ b/cypress/support/step_definitions/Search.Results/I_should_see_the_{string}_tab_as_active.js @@ -0,0 +1,5 @@ +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' + +defineStep('I should see the {string} tab as active', (type) => { + cy.get(`[data-test="${type}-tab"]`).should('have.class', '--active') +}) diff --git a/cypress/support/step_definitions/Search.Results/I_should_see_{int}_group_results.js b/cypress/support/step_definitions/Search.Results/I_should_see_{int}_group_results.js new file mode 100644 index 000000000..8888457bf --- /dev/null +++ b/cypress/support/step_definitions/Search.Results/I_should_see_{int}_group_results.js @@ -0,0 +1,5 @@ +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' + +defineStep('I should see {int} group results', (count) => { + cy.get('.group-teaser').should('have.length', count) +}) diff --git a/cypress/support/step_definitions/Search.Results/I_should_see_{int}_hashtag_results.js b/cypress/support/step_definitions/Search.Results/I_should_see_{int}_hashtag_results.js new file mode 100644 index 000000000..b42733c84 --- /dev/null +++ b/cypress/support/step_definitions/Search.Results/I_should_see_{int}_hashtag_results.js @@ -0,0 +1,5 @@ +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' + +defineStep('I should see {int} hashtag results', (count) => { + cy.get('.hc-hashtag').should('have.length', count) +}) diff --git a/cypress/support/step_definitions/Search.Results/I_should_see_{int}_post_results.js b/cypress/support/step_definitions/Search.Results/I_should_see_{int}_post_results.js new file mode 100644 index 000000000..2793b961f --- /dev/null +++ b/cypress/support/step_definitions/Search.Results/I_should_see_{int}_post_results.js @@ -0,0 +1,5 @@ +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' + +defineStep('I should see {int} post results', (count) => { + cy.get('.post-teaser').should('have.length', count) +}) diff --git a/cypress/support/step_definitions/Search.Results/I_should_see_{int}_user_results.js b/cypress/support/step_definitions/Search.Results/I_should_see_{int}_user_results.js new file mode 100644 index 000000000..54259f8a3 --- /dev/null +++ b/cypress/support/step_definitions/Search.Results/I_should_see_{int}_user_results.js @@ -0,0 +1,5 @@ +import { defineStep } from '@badeball/cypress-cucumber-preprocessor' + +defineStep('I should see {int} user results', (count) => { + cy.get('.user-teaser').should('have.length', count) +}) diff --git a/cypress/support/step_definitions/common/the_following_{string}_are_in_the_database.js b/cypress/support/step_definitions/common/the_following_{string}_are_in_the_database.js index 94c647745..b9639494e 100644 --- a/cypress/support/step_definitions/common/the_following_{string}_are_in_the_database.js +++ b/cypress/support/step_definitions/common/the_following_{string}_are_in_the_database.js @@ -32,6 +32,15 @@ defineStep('the following {string} are in the database:', (table,data) => { cy.factory().build('tag', entry, entry) }) break + case 'groups': + data.hashes().forEach( entry => { + cy.factory().build('group', { + ...entry, + deleted: Boolean(entry.deleted), + disabled: Boolean(entry.disabled), + }, entry) + }) + break case 'donations': data.hashes().forEach( entry => { cy.factory().build('donations', entry, entry) diff --git a/webapp/components/_new/features/SearchResults/SearchResults.vue b/webapp/components/_new/features/SearchResults/SearchResults.vue index cb87b2624..2c73bac74 100644 --- a/webapp/components/_new/features/SearchResults/SearchResults.vue +++ b/webapp/components/_new/features/SearchResults/SearchResults.vue @@ -1,121 +1,110 @@ @@ -474,4 +463,34 @@ export default { padding: 0; margin: 0; } + +.search-results-empty { + padding-top: $space-small; + + @media (max-width: 810px) { + padding-top: $space-x-small; + } +} + +.search-results-pagination { + padding-top: $space-small; + display: flex; + justify-content: center; + + @media (max-width: 810px) { + padding-top: $space-x-small; + } +} + +.search-results-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); + gap: $space-small; + padding-top: $space-small; + + @media (max-width: 810px) { + gap: $space-x-small; + padding-top: $space-x-small; + } +} diff --git a/webapp/components/_new/generic/TabNavigation/TabNavigation.vue b/webapp/components/_new/generic/TabNavigation/TabNavigation.vue index 00b2ea637..7b08fef58 100644 --- a/webapp/components/_new/generic/TabNavigation/TabNavigation.vue +++ b/webapp/components/_new/generic/TabNavigation/TabNavigation.vue @@ -1,5 +1,5 @@