diff --git a/backend/src/db/seed.js b/backend/src/db/seed.js
index 685b5ef0e..f562b5f15 100644
--- a/backend/src/db/seed.js
+++ b/backend/src/db/seed.js
@@ -931,6 +931,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
const additionalUsers = await Promise.all(
[...Array(30).keys()].map(() => Factory.build('user')),
)
+
await Promise.all(
additionalUsers.map(async (user) => {
await jennyRostock.relateTo(user, 'following')
@@ -938,6 +939,26 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
}),
)
+ await Promise.all(
+ [...Array(30).keys()].map((index) => Factory.build('user', { name: `Jenny${index}` })),
+ )
+
+ await Promise.all(
+ [...Array(30).keys()].map(() =>
+ Factory.build(
+ 'post',
+ { content: `Jenny ${faker.lorem.sentence()}` },
+ {
+ categoryIds: ['cat1'],
+ author: jennyRostock,
+ image: Factory.build('image', {
+ url: faker.image.unsplash.objects(),
+ }),
+ },
+ ),
+ ),
+ )
+
await Promise.all(
[...Array(30).keys()].map(() =>
Factory.build(
diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js
index f4f8c654b..528dfb233 100644
--- a/backend/src/middleware/permissionsMiddleware.js
+++ b/backend/src/middleware/permissionsMiddleware.js
@@ -86,7 +86,10 @@ export default shield(
'*': deny,
findPosts: allow,
findUsers: allow,
- findResources: allow,
+ searchResults: allow,
+ searchPosts: allow,
+ searchUsers: allow,
+ searchHashtags: allow,
embed: allow,
Category: allow,
Tag: allow,
diff --git a/backend/src/schema/resolvers/searches.js b/backend/src/schema/resolvers/searches.js
index 58fa63f8d..7b157fc65 100644
--- a/backend/src/schema/resolvers/searches.js
+++ b/backend/src/schema/resolvers/searches.js
@@ -3,90 +3,200 @@ import { queryString } from './searches/queryString'
// see http://lucene.apache.org/core/8_3_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package.description
+const cypherTemplate = (setup) => `
+ CALL db.index.fulltext.queryNodes('${setup.fulltextIndex}', $query)
+ YIELD node AS resource, score
+ ${setup.match}
+ ${setup.whereClause}
+ ${setup.withClause}
+ RETURN
+ ${setup.returnClause}
+ AS result
+ SKIP $skip
+ ${setup.limit}
+`
+
+const simpleWhereClause =
+ 'WHERE score >= 0.0 AND NOT (resource.deleted = true OR resource.disabled = true)'
+
+const postWhereClause = `WHERE score >= 0.0
+ AND NOT (
+ author.deleted = true OR author.disabled = true
+ OR resource.deleted = true OR resource.disabled = true
+ OR (:User {id: $userId})-[:MUTED]->(author)
+ )`
+
+const searchPostsSetup = {
+ fulltextIndex: 'post_fulltext_search',
+ match: 'MATCH (resource:Post)<-[:WROTE]-(author:User)',
+ whereClause: postWhereClause,
+ withClause: `WITH resource, author,
+ [(resource)<-[:COMMENTS]-(comment:Comment) | comment] AS comments,
+ [(resource)<-[:SHOUTED]-(user:User) | user] AS shouter`,
+ returnClause: `resource {
+ .*,
+ __typename: labels(resource)[0],
+ author: properties(author),
+ commentsCount: toString(size(comments)),
+ shoutedCount: toString(size(shouter))
+ }`,
+ limit: 'LIMIT $limit',
+}
+
+const searchUsersSetup = {
+ fulltextIndex: 'user_fulltext_search',
+ match: 'MATCH (resource:User)',
+ whereClause: simpleWhereClause,
+ withClause: '',
+ returnClause: 'resource {.*, __typename: labels(resource)[0]}',
+ limit: 'LIMIT $limit',
+}
+
+const searchHashtagsSetup = {
+ fulltextIndex: 'tag_fulltext_search',
+ match: 'MATCH (resource:Tag)',
+ whereClause: simpleWhereClause,
+ withClause: '',
+ returnClause: 'resource {.*, __typename: labels(resource)[0]}',
+ limit: 'LIMIT $limit',
+}
+
+const countSetup = {
+ returnClause: 'toString(size(collect(resource)))',
+ limit: '',
+}
+
+const countUsersSetup = {
+ ...searchUsersSetup,
+ ...countSetup,
+}
+const countPostsSetup = {
+ ...searchPostsSetup,
+ ...countSetup,
+}
+const countHashtagsSetup = {
+ ...searchHashtagsSetup,
+ ...countSetup,
+}
+
+const searchResultPromise = async (session, setup, params) => {
+ return session.readTransaction(async (transaction) => {
+ return transaction.run(cypherTemplate(setup), params)
+ })
+}
+
+const searchResultCallback = (result) => {
+ return result.records.map((r) => r.get('result'))
+}
+
+const countResultCallback = (result) => {
+ return result.records[0].get('result')
+}
+
+const getSearchResults = async (context, setup, params, resultCallback = searchResultCallback) => {
+ const session = context.driver.session()
+ try {
+ const results = await searchResultPromise(session, setup, params)
+ log(results)
+ return resultCallback(results)
+ } finally {
+ session.close()
+ }
+}
+
+const multiSearchMap = [
+ { symbol: '!', setup: searchPostsSetup, resultName: 'posts' },
+ { symbol: '@', setup: searchUsersSetup, resultName: 'users' },
+ { symbol: '#', setup: searchHashtagsSetup, resultName: 'hashtags' },
+]
+
export default {
Query: {
- findResources: async (_parent, args, context, _resolveInfo) => {
+ searchPosts: async (_parent, args, context, _resolveInfo) => {
+ const { query, postsOffset, firstPosts } = args
+ const { id: userId } = context.user
+
+ return {
+ postCount: getSearchResults(
+ context,
+ countPostsSetup,
+ {
+ query: queryString(query),
+ skip: 0,
+ userId,
+ },
+ countResultCallback,
+ ),
+ posts: getSearchResults(context, searchPostsSetup, {
+ query: queryString(query),
+ skip: postsOffset,
+ limit: firstPosts,
+ userId,
+ }),
+ }
+ },
+ searchUsers: async (_parent, args, context, _resolveInfo) => {
+ const { query, usersOffset, firstUsers } = args
+ return {
+ userCount: getSearchResults(
+ context,
+ countUsersSetup,
+ {
+ query: queryString(query),
+ skip: 0,
+ },
+ countResultCallback,
+ ),
+ users: getSearchResults(context, searchUsersSetup, {
+ query: queryString(query),
+ skip: usersOffset,
+ limit: firstUsers,
+ }),
+ }
+ },
+ searchHashtags: async (_parent, args, context, _resolveInfo) => {
+ const { query, hashtagsOffset, firstHashtags } = args
+ return {
+ hashtagCount: getSearchResults(
+ context,
+ countHashtagsSetup,
+ {
+ query: queryString(query),
+ skip: 0,
+ },
+ countResultCallback,
+ ),
+ hashtags: getSearchResults(context, searchHashtagsSetup, {
+ query: queryString(query),
+ skip: hashtagsOffset,
+ limit: firstHashtags,
+ }),
+ }
+ },
+ searchResults: async (_parent, args, context, _resolveInfo) => {
const { query, limit } = args
- const { id: thisUserId } = context.user
+ const { id: userId } = context.user
- const postCypher = `
- CALL db.index.fulltext.queryNodes('post_fulltext_search', $query)
- YIELD node as resource, score
- MATCH (resource)<-[:WROTE]-(author:User)
- WHERE score >= 0.0
- AND NOT (
- author.deleted = true OR author.disabled = true
- OR resource.deleted = true OR resource.disabled = true
- OR (:User {id: $thisUserId})-[:MUTED]->(author)
- )
- WITH resource, author,
- [(resource)<-[:COMMENTS]-(comment:Comment) | comment] as comments,
- [(resource)<-[:SHOUTED]-(user:User) | user] as shouter
- RETURN resource {
- .*,
- __typename: labels(resource)[0],
- author: properties(author),
- commentsCount: toString(size(comments)),
- shoutedCount: toString(size(shouter))
+ const searchType = query.replace(/^([!@#]?).*$/, '$1')
+ const searchString = query.replace(/^([!@#])/, '')
+
+ const params = {
+ query: queryString(searchString),
+ skip: 0,
+ limit,
+ userId,
}
- LIMIT $limit
- `
- const userCypher = `
- CALL db.index.fulltext.queryNodes('user_fulltext_search', $query)
- YIELD node as resource, score
- MATCH (resource)
- WHERE score >= 0.0
- AND NOT (resource.deleted = true OR resource.disabled = true)
- RETURN resource {.*, __typename: labels(resource)[0]}
- LIMIT $limit
- `
- const tagCypher = `
- CALL db.index.fulltext.queryNodes('tag_fulltext_search', $query)
- YIELD node as resource, score
- MATCH (resource)
- WHERE score >= 0.0
- AND NOT (resource.deleted = true OR resource.disabled = true)
- RETURN resource {.*, __typename: labels(resource)[0]}
- LIMIT $limit
- `
+ if (searchType === '')
+ return [
+ ...(await getSearchResults(context, searchPostsSetup, params)),
+ ...(await getSearchResults(context, searchUsersSetup, params)),
+ ...(await getSearchResults(context, searchHashtagsSetup, params)),
+ ]
- const myQuery = queryString(query)
-
- const session = context.driver.session()
- const searchResultPromise = session.readTransaction(async (transaction) => {
- const postTransactionResponse = transaction.run(postCypher, {
- query: myQuery,
- limit,
- thisUserId,
- })
- const userTransactionResponse = transaction.run(userCypher, {
- query: myQuery,
- limit,
- thisUserId,
- })
- const tagTransactionResponse = transaction.run(tagCypher, {
- query: myQuery,
- limit,
- })
- return Promise.all([
- postTransactionResponse,
- userTransactionResponse,
- tagTransactionResponse,
- ])
- })
-
- try {
- const [postResults, userResults, tagResults] = await searchResultPromise
- log(postResults)
- log(userResults)
- log(tagResults)
- return [...postResults.records, ...userResults.records, ...tagResults.records].map((r) =>
- r.get('resource'),
- )
- } finally {
- session.close()
- }
+ params.limit = 15
+ const type = multiSearchMap.find((obj) => obj.symbol === searchType)
+ return getSearchResults(context, type.setup, params)
},
},
}
diff --git a/backend/src/schema/resolvers/searches.spec.js b/backend/src/schema/resolvers/searches.spec.js
index 3d7bd039d..a859bf296 100644
--- a/backend/src/schema/resolvers/searches.spec.js
+++ b/backend/src/schema/resolvers/searches.spec.js
@@ -29,7 +29,7 @@ afterAll(async () => {
const searchQuery = gql`
query($query: String!) {
- findResources(query: $query, limit: 5) {
+ searchResults(query: $query, limit: 5) {
__typename
... on Post {
id
@@ -47,6 +47,21 @@ const searchQuery = gql`
}
}
`
+
+const searchPostQuery = gql`
+ query($query: String!, $firstPosts: Int, $postsOffset: Int) {
+ searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) {
+ postCount
+ posts {
+ __typename
+ id
+ title
+ content
+ }
+ }
+ }
+`
+
describe('resolvers/searches', () => {
let variables
@@ -65,7 +80,7 @@ describe('resolvers/searches', () => {
variables = { query: 'John' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
id: 'a-user',
name: 'John Doe',
@@ -95,7 +110,7 @@ describe('resolvers/searches', () => {
variables = { query: 'beitrag' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
__typename: 'Post',
id: 'a-post',
@@ -114,7 +129,7 @@ describe('resolvers/searches', () => {
variables = { query: 'BEITRAG' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
__typename: 'Post',
id: 'a-post',
@@ -132,7 +147,7 @@ describe('resolvers/searches', () => {
it('returns empty search results', async () => {
await expect(
query({ query: searchQuery, variables: { query: 'Unfug' } }),
- ).resolves.toMatchObject({ data: { findResources: [] } })
+ ).resolves.toMatchObject({ data: { searchResults: [] } })
})
})
@@ -189,7 +204,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: 'beitrag' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: expect.arrayContaining([
+ searchResults: expect.arrayContaining([
{
__typename: 'Post',
id: 'a-post',
@@ -216,7 +231,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: 'tee-ei' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
__typename: 'Post',
id: 'g-post',
@@ -235,7 +250,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: '„teeei“' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
__typename: 'Post',
id: 'g-post',
@@ -256,7 +271,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: '(a - b)²' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
__typename: 'Post',
id: 'c-post',
@@ -277,7 +292,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: '(a-b)²' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
__typename: 'Post',
id: 'c-post',
@@ -298,7 +313,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: '+ b² 2.' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
__typename: 'Post',
id: 'c-post',
@@ -321,7 +336,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: 'der panther' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
__typename: 'Post',
id: 'd-post',
@@ -349,7 +364,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: 'Vorü Subs' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: expect.arrayContaining([
+ searchResults: expect.arrayContaining([
{
__typename: 'Post',
id: 'd-post',
@@ -395,7 +410,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: '-maria-' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: expect.arrayContaining([
+ searchResults: expect.arrayContaining([
{
__typename: 'User',
id: 'c-user',
@@ -416,6 +431,128 @@ und hinter tausend Stäben keine Welt.`,
})
})
+ describe('adding a user and a hashtag with a name that is content of a post', () => {
+ beforeAll(async () => {
+ await Promise.all([
+ Factory.build('user', {
+ id: 'f-user',
+ name: 'Peter Panther',
+ slug: 'peter-panther',
+ }),
+ await Factory.build('tag', { id: 'Panther' }),
+ ])
+ })
+
+ describe('query the word that contains the post, the hashtag and the name of the user', () => {
+ it('finds the user, the post and the hashtag', async () => {
+ variables = { query: 'panther' }
+ await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
+ data: {
+ searchResults: expect.arrayContaining([
+ {
+ __typename: 'User',
+ id: 'f-user',
+ name: 'Peter Panther',
+ slug: 'peter-panther',
+ },
+ {
+ __typename: 'Post',
+ id: 'd-post',
+ title: 'Der Panther',
+ content: `Sein Blick ist vom Vorübergehn der Stäbe
+so müd geworden, daß er nichts mehr hält.
+Ihm ist, als ob es tausend Stäbe gäbe
+und hinter tausend Stäben keine Welt.`,
+ },
+ {
+ __typename: 'Tag',
+ id: 'Panther',
+ },
+ ]),
+ },
+ errors: undefined,
+ })
+ })
+ })
+
+ describe('@query the word that contains the post, the hashtag and the name of the user', () => {
+ it('only finds the user', async () => {
+ variables = { query: '@panther' }
+ await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
+ data: {
+ searchResults: expect.not.arrayContaining([
+ {
+ __typename: 'Post',
+ id: 'd-post',
+ title: 'Der Panther',
+ content: `Sein Blick ist vom Vorübergehn der Stäbe
+so müd geworden, daß er nichts mehr hält.
+Ihm ist, als ob es tausend Stäbe gäbe
+und hinter tausend Stäben keine Welt.`,
+ },
+ {
+ __typename: 'Tag',
+ id: 'Panther',
+ },
+ ]),
+ },
+ errors: undefined,
+ })
+ })
+ })
+
+ describe('!query the word that contains the post, the hashtag and the name of the user', () => {
+ it('only finds the post', async () => {
+ variables = { query: '!panther' }
+ await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
+ data: {
+ searchResults: expect.not.arrayContaining([
+ {
+ __typename: 'User',
+ id: 'f-user',
+ name: 'Peter Panther',
+ slug: 'peter-panther',
+ },
+ {
+ __typename: 'Tag',
+ id: 'Panther',
+ },
+ ]),
+ },
+ errors: undefined,
+ })
+ })
+ })
+
+ describe('#query the word that contains the post, the hashtag and the name of the user', () => {
+ it('only finds the hashtag', async () => {
+ variables = { query: '#panther' }
+ await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
+ data: {
+ searchResults: expect.not.arrayContaining([
+ {
+ __typename: 'User',
+ id: 'f-user',
+ name: 'Peter Panther',
+ slug: 'peter-panther',
+ },
+ {
+ __typename: 'Post',
+ id: 'd-post',
+ title: 'Der Panther',
+ content: `Sein Blick ist vom Vorübergehn der Stäbe
+so müd geworden, daß er nichts mehr hält.
+Ihm ist, als ob es tausend Stäbe gäbe
+und hinter tausend Stäben keine Welt.`,
+ },
+ ]),
+ },
+ errors: undefined,
+ })
+ })
+ })
+ })
+
describe('adding a post, written by a user who is muted by the authenticated user', () => {
beforeAll(async () => {
const mutedUser = await Factory.build('user', {
@@ -440,7 +577,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: 'beitrag' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: expect.not.arrayContaining([
+ searchResults: expect.not.arrayContaining([
{
__typename: 'Post',
id: 'muted-post',
@@ -465,7 +602,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: 'myha' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
__typename: 'Tag',
id: 'myHashtag',
@@ -477,6 +614,30 @@ und hinter tausend Stäben keine Welt.`,
})
})
})
+
+ describe('searchPostQuery', () => {
+ describe('query with limit 1', () => {
+ it('has a count greater than 1', async () => {
+ variables = { query: 'beitrag', firstPosts: 1, postsOffset: 0 }
+ await expect(query({ query: searchPostQuery, variables })).resolves.toMatchObject({
+ data: {
+ searchPosts: {
+ postCount: 2,
+ posts: [
+ {
+ __typename: 'Post',
+ id: 'a-post',
+ title: 'Beitrag',
+ content: 'Ein erster Beitrag',
+ },
+ ],
+ },
+ },
+ errors: undefined,
+ })
+ })
+ })
+ })
})
})
})
diff --git a/backend/src/schema/resolvers/searches/queryString.js b/backend/src/schema/resolvers/searches/queryString.js
index 064f17f48..3f2f70d48 100644
--- a/backend/src/schema/resolvers/searches/queryString.js
+++ b/backend/src/schema/resolvers/searches/queryString.js
@@ -39,7 +39,8 @@ const matchBeginningOfWords = (str) => {
}
export function normalizeWhitespace(str) {
- return str.replace(/\s+/g, ' ').trim()
+ // delete the first character if it is !, @ or #
+ return str.replace(/^([!@#])/, '').replace(/\s+/g, ' ').trim()
}
export function escapeSpecialCharacters(str) {
diff --git a/backend/src/schema/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js
index cce45ae6e..6509f0e68 100644
--- a/backend/src/schema/resolvers/users.spec.js
+++ b/backend/src/schema/resolvers/users.spec.js
@@ -265,9 +265,9 @@ describe('UpdateUser', () => {
})
it('supports updating location', async () => {
- variables = { ...variables, locationName: 'Hamburg, New Jersey, United States of America' }
+ variables = { ...variables, locationName: 'Hamburg, New Jersey, United States' }
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({
- data: { UpdateUser: { locationName: 'Hamburg, New Jersey, United States of America' } },
+ data: { UpdateUser: { locationName: 'Hamburg, New Jersey, United States' } },
errors: undefined,
})
})
diff --git a/backend/src/schema/types/type/Search.gql b/backend/src/schema/types/type/Search.gql
index 1ce38001d..9537b5a84 100644
--- a/backend/src/schema/types/type/Search.gql
+++ b/backend/src/schema/types/type/Search.gql
@@ -1,5 +1,23 @@
union SearchResult = Post | User | Tag
-type Query {
- findResources(query: String!, limit: Int = 5): [SearchResult]!
+type postSearchResults {
+ postCount: Int
+ posts: [Post]!
+}
+
+type userSearchResults {
+ userCount: Int
+ users: [User]!
+}
+
+type hashtagSearchResults {
+ hashtagCount: Int
+ hashtags: [Tag]!
+}
+
+type Query {
+ searchPosts(query: String!, firstPosts: Int, postsOffset: Int): postSearchResults!
+ searchUsers(query: String!, firstUsers: Int, usersOffset: Int): userSearchResults!
+ searchHashtags(query: String!, firstHashtags: Int, hashtagsOffset: Int): hashtagSearchResults!
+ searchResults(query: String!, limit: Int = 5): [SearchResult]!
}
diff --git a/cypress/integration/common/search.js b/cypress/integration/common/search.js
index 1feece77e..5eae20a22 100644
--- a/cypress/integration/common/search.js
+++ b/cypress/integration/common/search.js
@@ -37,7 +37,7 @@ Then("I should see the following posts in the select dropdown:", table => {
});
Then("I should see the following users in the select dropdown:", table => {
- cy.get(".ds-heading").should("contain", "Users");
+ cy.get(".search-heading").should("contain", "Users");
table.hashes().forEach(({ slug }) => {
cy.get(".ds-select-dropdown").should("contain", slug);
});
@@ -85,6 +85,26 @@ Then(
}
);
+Then("I should see the search results page", () => {
+ cy.location("pathname").should(
+ "eq",
+ "/search/search-results"
+ );
+ cy.location("search").should(
+ "eq",
+ "?search=PR"
+ );
+});
+
+Then("I should see the following posts on the search results page",
+ () => {
+ cy.get(".post-teaser").should(
+ "contain",
+ "101 Essays that will change the way you think"
+ );
+ }
+);
+
Then(
"I should not see posts without the searched-for term in the select dropdown",
() => {
diff --git a/cypress/integration/search/Search.feature b/cypress/integration/search/Search.feature
index b77b45d8e..d128838f3 100644
--- a/cypress/integration/search/Search.feature
+++ b/cypress/integration/search/Search.feature
@@ -8,7 +8,7 @@ Feature: Search
And we have the following posts in our database:
| id | title | content |
| p1 | 101 Essays that will change the way you think | 101 Essays, of course (PR)! |
- | p2 | No searched for content | will be found in this post, I guarantee |
+ | p2 | No content | will be found in this post, I guarantee |
And we have the following user accounts:
| slug | name | id |
| search-for-me | Search for me | user-for-search |
@@ -23,10 +23,10 @@ Feature: Search
| title |
| 101 Essays that will change the way you think |
- Scenario: Press enter starts search
+ Scenario: Press enter opens search page
When I type "PR" and press Enter
- Then I should have one item in the select dropdown
- Then I should see the following posts in the select dropdown:
+ Then I should see the search results page
+ Then I should see the following posts on the search results page
| title |
| 101 Essays that will change the way you think |
diff --git a/webapp/assets/_new/styles/resets.scss b/webapp/assets/_new/styles/resets.scss
index 72a6184b3..4545634cc 100644
--- a/webapp/assets/_new/styles/resets.scss
+++ b/webapp/assets/_new/styles/resets.scss
@@ -16,13 +16,14 @@ h3,
h4,
h5,
h6,
-p {
+p,
+li {
margin: 0;
}
-ul,
-ol {
+ol,
+ul {
list-style-type: none;
- padding: 0;
margin: 0;
+ padding: 0;
}
diff --git a/webapp/assets/_new/styles/tokens.scss b/webapp/assets/_new/styles/tokens.scss
index 74699a097..5ab1e8aef 100644
--- a/webapp/assets/_new/styles/tokens.scss
+++ b/webapp/assets/_new/styles/tokens.scss
@@ -251,7 +251,7 @@ $size-ribbon: 6px;
*/
$size-width-filter-sidebar: 85px;
-$size-width-paginate: 100px;
+$size-width-paginate: 200px;
$size-max-width-filter-menu: 1026px;
/**
diff --git a/webapp/components/_new/features/SearchResults/SearchResults.spec.js b/webapp/components/_new/features/SearchResults/SearchResults.spec.js
new file mode 100644
index 000000000..91c12be14
--- /dev/null
+++ b/webapp/components/_new/features/SearchResults/SearchResults.spec.js
@@ -0,0 +1,234 @@
+import { config, mount } from '@vue/test-utils'
+import Vuex from 'vuex'
+import SearchResults from './SearchResults'
+import helpers from '~/storybook/helpers'
+
+helpers.init()
+
+const localVue = global.localVue
+
+localVue.directive('scrollTo', jest.fn())
+
+config.stubs['client-only'] = '