Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-02-08 16:02:35 +07:00 committed by GitHub
parent bff122ee8c
commit 9c278d46e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 185 additions and 64 deletions

View File

@ -14,8 +14,8 @@
//
import type { IntlString } from '@anticrm/platform'
import { Builder, Model, Prop, UX, TypeString, TypeTimestamp } from '@anticrm/model'
import type { Domain } from '@anticrm/core'
import { Builder, Model, Prop, UX, TypeString, TypeTimestamp, Index } from '@anticrm/model'
import { Domain, IndexKind } from '@anticrm/core'
import core, { TAttachedDoc } from '@anticrm/model-core'
import type { Attachment, Photo } from '@anticrm/attachment'
import activity from '@anticrm/activity'
@ -31,6 +31,7 @@ export const DOMAIN_ATTACHMENT = 'attachment' as Domain
@UX('File' as IntlString)
export class TAttachment extends TAttachedDoc implements Attachment {
@Prop(TypeString(), 'Name' as IntlString)
@Index(IndexKind.FullText)
name!: string
@Prop(TypeString(), 'File' as IntlString)
@ -40,6 +41,7 @@ export class TAttachment extends TAttachedDoc implements Attachment {
size!: number
@Prop(TypeString(), 'Type' as IntlString)
@Index(IndexKind.FullText)
type!: string
@Prop(TypeTimestamp(), 'Date' as IntlString)

View File

@ -59,6 +59,7 @@ export class TContact extends TDoc implements Contact {
comments?: number
@Prop(TypeString(), 'Location' as IntlString)
@Index(IndexKind.FullText)
city!: string
}
@ -69,6 +70,7 @@ export class TChannel extends TAttachedDoc implements Channel {
provider!: Ref<ChannelProvider>
@Prop(TypeString(), 'Value' as IntlString)
@Index(IndexKind.FullText)
value!: string
items?: number

View File

@ -13,9 +13,8 @@
// limitations under the License.
//
import type { Account, Arr, Ref, Space } from '@anticrm/core'
import { DOMAIN_MODEL } from '@anticrm/core'
import { Model, Prop, TypeBoolean, TypeString } from '@anticrm/model'
import { Account, Arr, DOMAIN_MODEL, IndexKind, Ref, Space } from '@anticrm/core'
import { Index, Model, Prop, TypeBoolean, TypeString } from '@anticrm/model'
import type { IntlString } from '@anticrm/platform'
import core from './component'
import { TDoc } from './core'
@ -25,9 +24,11 @@ import { TDoc } from './core'
@Model(core.class.Space, core.class.Doc, DOMAIN_MODEL)
export class TSpace extends TDoc implements Space {
@Prop(TypeString(), 'Name' as IntlString)
@Index(IndexKind.FullText)
name!: string
@Prop(TypeString(), 'Description' as IntlString)
@Index(IndexKind.FullText)
description!: string
@Prop(TypeBoolean(), 'Private' as IntlString)

View File

@ -15,12 +15,12 @@
//
import type { IntlString } from '@anticrm/platform'
import { Builder, Model, TypeString, Prop, ArrOf, TypeBoolean } from '@anticrm/model'
import { Builder, Model, TypeString, Prop, ArrOf, TypeBoolean, Index } from '@anticrm/model'
import core, { TAttachedDoc } from '@anticrm/model-core'
import contact from '@anticrm/model-contact'
import gmail from './plugin'
import type { Message, SharedMessage, SharedMessages } from '@anticrm/gmail'
import type { Domain, Type } from '@anticrm/core'
import { Domain, IndexKind, Type } from '@anticrm/core'
import setting from '@anticrm/setting'
import activity from '@anticrm/activity'
@ -36,24 +36,31 @@ export class TMessage extends TAttachedDoc implements Message {
messageId!: string
@Prop(TypeString(), 'ReplyTo' as IntlString)
@Index(IndexKind.FullText)
replyTo?: string
@Prop(TypeString(), 'From' as IntlString)
@Index(IndexKind.FullText)
from!: string
@Prop(TypeString(), 'To' as IntlString)
@Index(IndexKind.FullText)
to!: string
@Prop(TypeString(), 'Contact' as IntlString)
@Index(IndexKind.FullText)
contact!: string
@Prop(TypeString(), 'Subject' as IntlString)
@Index(IndexKind.FullText)
subject!: string
@Prop(TypeString(), 'Message' as IntlString)
@Index(IndexKind.FullText)
content!: string
@Prop(TypeString(), 'Message' as IntlString)
@Index(IndexKind.FullText)
textContent!: string
@Prop(ArrOf(TypeString()), 'Copy' as IntlString)

View File

@ -14,9 +14,9 @@
// limitations under the License.
//
import { Doc, Domain, FindOptions, Ref } from '@anticrm/core'
import { Doc, Domain, FindOptions, IndexKind, Ref } from '@anticrm/core'
import type { Category, Product, Variant } from '@anticrm/inventory'
import { Builder, Collection, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model'
import { Builder, Collection, Index, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model'
import attachment from '@anticrm/model-attachment'
import core, { TAttachedDoc } from '@anticrm/model-core'
import workbench from '@anticrm/model-workbench'
@ -30,6 +30,7 @@ export const DOMAIN_INVENTORY = 'inventory' as Domain
@UX(inventory.string.Category, inventory.icon.Categories, undefined, 'name')
export class TCategory extends TAttachedDoc implements Category {
@Prop(TypeString(), 'Name' as IntlString)
@Index(IndexKind.FullText)
name!: string
}
@ -41,6 +42,7 @@ export class TProduct extends TAttachedDoc implements Product {
declare attachedTo: Ref<Category>
@Prop(TypeString(), 'Name' as IntlString)
@Index(IndexKind.FullText)
name!: string
@Prop(Collection(attachment.class.Photo), attachment.string.Photos)
@ -61,9 +63,11 @@ export class TVariant extends TAttachedDoc implements Variant {
declare attachedTo: Ref<Product>
@Prop(TypeString(), 'Name' as IntlString)
@Index(IndexKind.FullText)
name!: string
@Prop(TypeString(), inventory.string.SKU)
@Index(IndexKind.FullText)
sku!: string
}

View File

@ -16,9 +16,9 @@
// To help typescript locate view plugin properly
import type { Employee } from '@anticrm/contact'
import type { Doc, FindOptions, Lookup, Ref } from '@anticrm/core'
import { Doc, FindOptions, IndexKind, Lookup, Ref } from '@anticrm/core'
import type { Customer, Funnel, Lead } from '@anticrm/lead'
import { Builder, Collection, Mixin, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model'
import { Builder, Collection, Index, Mixin, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model'
import attachment from '@anticrm/model-attachment'
import chunter from '@anticrm/model-chunter'
import contact, { TPerson } from '@anticrm/model-contact'
@ -41,6 +41,7 @@ export class TLead extends TTask implements Lead {
declare attachedTo: Ref<Customer>
@Prop(TypeString(), 'Title' as IntlString)
@Index(IndexKind.FullText)
title!: string
@Prop(Collection(chunter.class.Comment), 'Comments' as IntlString)
@ -60,6 +61,7 @@ export class TCustomer extends TPerson implements Customer {
leads?: number
@Prop(TypeString(), 'Description' as IntlString)
@Index(IndexKind.FullText)
description!: string
}

View File

@ -14,24 +14,25 @@
//
import type { Employee } from '@anticrm/contact'
import { Doc, FindOptions, Lookup, Ref, Timestamp } from '@anticrm/core'
import { Builder, Collection, Mixin, Model, Prop, TypeBoolean, TypeDate, TypeMarkup, TypeRef, TypeString, UX } from '@anticrm/model'
import { Doc, FindOptions, IndexKind, Lookup, Ref, Timestamp } from '@anticrm/core'
import { Builder, Collection, Index, Mixin, Model, Prop, TypeBoolean, TypeDate, TypeMarkup, TypeRef, TypeString, UX } from '@anticrm/model'
import attachment from '@anticrm/model-attachment'
import chunter from '@anticrm/model-chunter'
import contact, { TPerson } from '@anticrm/model-contact'
import core, { TSpace } from '@anticrm/model-core'
import presentation from '@anticrm/model-presentation'
import task, { TSpaceWithStates, TTask } from '@anticrm/model-task'
import view from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench'
import type { IntlString } from '@anticrm/platform'
import { Applicant, Candidate, Candidates, Vacancy } from '@anticrm/recruit'
import recruit from './plugin'
import presentation from '@anticrm/model-presentation'
@Model(recruit.class.Vacancy, task.class.SpaceWithStates)
@UX(recruit.string.Vacancy, recruit.icon.Vacancy)
export class TVacancy extends TSpaceWithStates implements Vacancy {
@Prop(TypeMarkup(), 'Full description' as IntlString)
@Index(IndexKind.FullText)
fullDescription?: string
@Prop(Collection(attachment.class.Attachment), 'Attachments' as IntlString)
@ -41,9 +42,11 @@ export class TVacancy extends TSpaceWithStates implements Vacancy {
dueTo?: Timestamp
@Prop(TypeString(), 'Location' as IntlString, recruit.icon.Location)
@Index(IndexKind.FullText)
location?: string
@Prop(TypeString(), 'Company' as IntlString, contact.icon.Company)
@Index(IndexKind.FullText)
company?: string
}
@ -55,6 +58,7 @@ export class TCandidates extends TSpace implements Candidates {}
@UX('Candidate' as IntlString, recruit.icon.RecruitApplication)
export class TCandidate extends TPerson implements Candidate {
@Prop(TypeString(), 'Title' as IntlString)
@Index(IndexKind.FullText)
title?: string
@Prop(Collection(recruit.class.Applicant), 'Applications' as IntlString)
@ -67,6 +71,7 @@ export class TCandidate extends TPerson implements Candidate {
remote?: boolean
@Prop(TypeString(), 'Source' as IntlString)
@Index(IndexKind.FullText)
source?: string
}
@ -313,6 +318,6 @@ export function createModel (builder: Builder): void {
})
}
export { default } from './plugin'
export { recruitOperation } from './migration'
export { createDeps } from './creation'
export { recruitOperation } from './migration'
export { default } from './plugin'

View File

@ -15,10 +15,11 @@
import type { Employee } from '@anticrm/contact'
import contact from '@anticrm/contact'
import { Arr, Class, Doc, Domain, DOMAIN_MODEL, FindOptions, Ref, Space, Timestamp } from '@anticrm/core'
import { Arr, Class, Doc, Domain, DOMAIN_MODEL, FindOptions, IndexKind, Ref, Space, Timestamp } from '@anticrm/core'
import {
Builder,
Collection, Hidden, Implements,
Index,
Mixin,
Model,
Prop, TypeBoolean,
@ -106,6 +107,7 @@ export class TTask extends TAttachedDoc implements Task {
@UX(task.string.Todo)
export class TTodoItem extends TAttachedDoc implements TodoItem {
@Prop(TypeString(), task.string.TodoName, task.icon.Task)
@Index(IndexKind.FullText)
name!: string
@Prop(TypeBoolean(), task.string.TaskDone)
@ -130,9 +132,11 @@ export class TIssue extends TTask implements Issue {
declare attachedTo: Ref<Doc>
@Prop(TypeString(), task.string.IssueName)
@Index(IndexKind.FullText)
name!: string
@Prop(TypeMarkup(), task.string.TaskDescription)
@Index(IndexKind.FullText)
description!: string
@Prop(Collection(chunter.class.Comment), task.string.TaskComments)
@ -142,6 +146,7 @@ export class TIssue extends TTask implements Issue {
attachments!: number
@Prop(TypeString(), task.string.TaskLabels)
@Index(IndexKind.FullText)
labels!: string
@Prop(TypeRef(contact.class.Employee), task.string.TaskAssignee)
@ -193,6 +198,7 @@ export class TLostStateTemplate extends TDoneStateTemplate implements LostStateT
@Model(task.class.KanbanTemplate, core.class.Doc, DOMAIN_KANBAN)
export class TKanbanTemplate extends TDoc implements KanbanTemplate {
@Prop(TypeString(), task.string.KanbanTemplateTitle)
@Index(IndexKind.FullText)
title!: string
@Prop(Collection(task.class.StateTemplate), task.string.States)

View File

@ -15,12 +15,12 @@
//
import type { IntlString } from '@anticrm/platform'
import { Builder, Model, TypeString, TypeBoolean, Prop, ArrOf } from '@anticrm/model'
import { Builder, Model, TypeString, TypeBoolean, Prop, ArrOf, Index } from '@anticrm/model'
import core, { TAttachedDoc } from '@anticrm/model-core'
import contact from '@anticrm/model-contact'
import telegram from './plugin'
import type { TelegramMessage, SharedTelegramMessage, SharedTelegramMessages } from '@anticrm/telegram'
import type { Domain, Type } from '@anticrm/core'
import { Domain, IndexKind, Type } from '@anticrm/core'
import setting from '@anticrm/setting'
import activity from '@anticrm/activity'
@ -33,6 +33,7 @@ function TypeSharedMessage (): Type<SharedTelegramMessage> {
@Model(telegram.class.Message, core.class.AttachedDoc, DOMAIN_TELEGRAM)
export class TTelegramMessage extends TAttachedDoc implements TelegramMessage {
@Prop(TypeString(), 'Content' as IntlString)
@Index(IndexKind.FullText)
content!: string
@Prop(TypeBoolean(), 'Incoming' as IntlString)

View File

@ -14,8 +14,8 @@
// limitations under the License.
//
import type { Domain } from '@anticrm/core'
import { Builder, Model, Prop, TypeString } from '@anticrm/model'
import { Domain, IndexKind } from '@anticrm/core'
import { Builder, Index, Model, Prop, TypeString } from '@anticrm/model'
import core, { TDoc } from '@anticrm/model-core'
import textEditor from '@anticrm/model-text-editor'
import { IntlString } from '@anticrm/platform'
@ -28,9 +28,11 @@ export const DOMAIN_TEMPLATES = 'templates' as Domain
@Model(templates.class.MessageTemplate, core.class.Doc, DOMAIN_TEMPLATES)
export class TMessageTemplate extends TDoc implements MessageTemplate {
@Prop(TypeString(), 'Title' as IntlString)
@Index(IndexKind.FullText)
title!: string;
@Prop(TypeString(), 'Message' as IntlString)
@Index(IndexKind.FullText)
message!: string;
}

View File

@ -22,7 +22,7 @@
import { AttributesBar, createQuery, getClient } from '@anticrm/presentation'
import { Vacancy } from '@anticrm/recruit'
import { StyledTextBox } from '@anticrm/text-editor'
import { Component, EditBox, Grid, Icon, IconClose, Label, ToggleWithLabel, ActionIcon, Scroller } from '@anticrm/ui'
import { ActionIcon, Component, EditBox, Grid, Icon, IconClose, Label, Scroller, ToggleWithLabel } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import recruit from '../plugin'
@ -108,7 +108,7 @@
<ToggleWithLabel label={recruit.string.ThisVacancyIsPrivate} description={recruit.string.MakePrivateDescription}/>
</Scroller>
{:else if selected === 2}
<Component is={activity.component.Activity} props={{object, transparent: true}} />
<Component is={activity.component.Activity} props={{ object, transparent: true }} />
{/if}
{/if}
</div>

View File

@ -15,34 +15,38 @@
//
import core, {
AttachedDoc, Class, ClassifierKind, Doc, Obj, Ref, TxCreateDoc, TxResult, TxUpdateDoc,
AnyAttribute,
AttachedDoc,
Class,
ClassifierKind,
Collection,
Doc,
DocumentQuery,
FindOptions,
FindResult,
Hierarchy,
IndexKind,
MeasureContext,
Obj,
PropertyType,
Ref,
Tx,
TxBulkWrite,
TxCollectionCUD,
TxCreateDoc,
TxMixin,
TxProcessor,
TxPutBag,
TxRemoveDoc
TxRemoveDoc,
TxResult,
TxUpdateDoc
} from '@anticrm/core'
import type { FullTextAdapter, IndexedDoc, WithFind } from './types'
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const NO_INDEX = [] as AnyAttribute[]
/**
* @public
*/
export class FullTextIndex implements WithFind {
private readonly indexes = new Map<Ref<Class<Obj>>, AnyAttribute[]>()
constructor (
private readonly hierarchy: Hierarchy,
private readonly adapter: FullTextAdapter,
@ -138,7 +142,7 @@ export class FullTextIndex implements WithFind {
const { _id, $search, ...mainQuery } = query
if ($search === undefined) return []
const docs = await this.adapter.search(_class, query, options?.limit)
const ids: Set<Ref<Doc>> = new Set<Ref<Doc>>(docs.map(p => p.id))
const ids: Set<Ref<Doc>> = new Set<Ref<Doc>>(docs.map((p) => p.id))
for (const doc of docs) {
if (doc.attachedTo !== undefined) {
ids.add(doc.attachedTo)
@ -147,25 +151,29 @@ export class FullTextIndex implements WithFind {
return await this.dbStorage.findAll(ctx, _class, { _id: { $in: Array.from(ids) as any }, ...mainQuery }, options) // TODO: remove `as any`
}
private getFullTextAttributes (clazz: Ref<Class<Obj>>): AnyAttribute[] | undefined {
const attributes = this.indexes.get(clazz)
if (attributes === undefined) {
const allAttributes = this.hierarchy.getAllAttributes(clazz)
const result: AnyAttribute[] = []
for (const [, attr] of allAttributes) {
if (attr.type._class === core.class.TypeString || attr.type._class === core.class.TypeMarkup) {
result.push(attr)
}
private getFullTextAttributes (clazz: Ref<Class<Obj>>, parentDoc?: Doc): AnyAttribute[] {
const allAttributes = this.hierarchy.getAllAttributes(clazz)
const result: AnyAttribute[] = []
for (const [, attr] of allAttributes) {
if (isFullTextAttribute(attr)) {
result.push(attr)
}
if (result.length > 0) {
this.indexes.set(clazz, result)
return result
} else {
this.indexes.set(clazz, NO_INDEX)
}
} else if (attributes !== NO_INDEX) {
return attributes
}
// We also neex to add all mixin attribues if parent is specified.
if (parentDoc !== undefined) {
this.hierarchy
.getDescendants(clazz)
.filter((m) => this.hierarchy.getClass(m).kind === ClassifierKind.MIXIN)
.forEach((m) => {
for (const [, v] of this.hierarchy.getAllAttributes(m, clazz)) {
if (isFullTextAttribute(v) && this.hierarchy.hasMixin(parentDoc, m)) {
result.push(v)
}
}
})
}
return result
}
protected async txCreateDoc (ctx: MeasureContext, tx: TxCreateDoc<Doc>): Promise<TxResult> {
@ -174,15 +182,20 @@ export class FullTextIndex implements WithFind {
let parentContent: Record<string, string> = {}
if (this.hierarchy.isDerived(doc._class, core.class.AttachedDoc)) {
const attachedDoc = doc as AttachedDoc
const parentDoc = (
await this.dbStorage.findAll(ctx, attachedDoc.attachedToClass, { _id: attachedDoc.attachedTo }, { limit: 1 })
)[0]
if (parentDoc !== undefined) {
const parentAttributes = this.getFullTextAttributes(parentDoc._class)
parentContent = this.getContent(parentAttributes, parentDoc)
if (attachedDoc.attachedToClass !== undefined && attachedDoc.attachedTo !== undefined) {
const parentDoc = (
await this.dbStorage.findAll(ctx, attachedDoc.attachedToClass, { _id: attachedDoc.attachedTo }, { limit: 1 })
)[0]
if (parentDoc !== undefined) {
const parentAttributes = this.getFullTextAttributes(parentDoc._class, parentDoc)
if (parentAttributes.length > 0) {
parentContent = this.getContent(parentAttributes, parentDoc)
}
}
}
}
if (attributes === undefined && Object.keys(parentContent).length === 0) return {}
if (attributes.length === 0 && Object.keys(parentContent).length === 0) return {}
let content = this.getContent(attributes, doc)
content = { ...parentContent, ...content }
@ -201,7 +214,7 @@ export class FullTextIndex implements WithFind {
protected async txUpdateDoc (ctx: MeasureContext, tx: TxUpdateDoc<Doc>): Promise<TxResult> {
const attributes = this.getFullTextAttributes(tx.objectClass)
let result = {}
if (attributes === undefined) return result
if (attributes.length === 0) return result
const ops: any = tx.operations
const update: any = {}
let shouldUpdate = false
@ -224,22 +237,33 @@ export class FullTextIndex implements WithFind {
return result
}
private getContent (attributes: AnyAttribute[] | undefined, doc: Doc): Record<string, string> {
private getContent (attributes: AnyAttribute[], doc: Doc): Record<string, string> {
const attrs: Record<string, string> = {}
for (const attr of attributes ?? []) {
attrs[attr.name] = (doc as any)[attr.name]?.toString() ?? ''
for (const attr of attributes) {
const isMixinAttr = this.hierarchy.isMixin(attr.attributeOf)
if (isMixinAttr) {
attrs[(attr.attributeOf as string) + '.' + attr.name] =
((doc as any)[attr.attributeOf] ?? {})[attr.name]?.toString() ?? ''
} else {
attrs[attr.name] = (doc as any)[attr.name]?.toString() ?? ''
}
}
return attrs
}
private async updateAttachedDocs (ctx: MeasureContext, tx: {objectId: Ref<Doc>, objectClass: Ref<Class<Doc>>}, update: any): Promise<void> {
private async updateAttachedDocs (
ctx: MeasureContext,
tx: { objectId: Ref<Doc>, objectClass: Ref<Class<Doc>> },
update: any
): Promise<void> {
const doc = (await this.dbStorage.findAll(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0]
if (doc === undefined) return
const attributes = this.hierarchy.getAllAttributes(doc._class)
// Find all mixin atttibutes for document.
this.hierarchy.getDescendants(doc._class)
this.hierarchy
.getDescendants(doc._class)
.filter((m) => this.hierarchy.getClass(m).kind === ClassifierKind.MIXIN && this.hierarchy.hasMixin(doc, m))
.forEach((m) => {
for (const [k, v] of this.hierarchy.getAllAttributes(m, doc._class)) {
@ -261,7 +285,14 @@ export class FullTextIndex implements WithFind {
await this.adapter.update(attached._id, docUpdate)
} catch (err: any) {
if (((err.message as string) ?? '').includes('document_missing_exception:')) {
console.error('missing document in elastic for', tx.objectId, 'attached', attached._id, 'collection', attached.collection)
console.error(
'missing document in elastic for',
tx.objectId,
'attached',
attached._id,
'collection',
attached.collection
)
// We have no document for attached object, so ignore for now. it is probable rebuild of elastic DB.
continue
}
@ -272,3 +303,9 @@ export class FullTextIndex implements WithFind {
}
}
}
function isFullTextAttribute (attr: AnyAttribute): boolean {
return (
attr.index === IndexKind.FullText &&
(attr.type._class === core.class.TypeString || attr.type._class === core.class.TypeMarkup)
)
}

View File

@ -11,7 +11,7 @@
"format": "prettier --write src && eslint --fix src",
"ci": "playwright install --with-deps chromium",
"test": "",
"uitest": "playwright test --browser chromium --reporter list,html"
"uitest": "playwright test --browser chromium --reporter list,html -c ./tests/playwright.config.ts"
},
"devDependencies": {
"@anticrm/platform-rig": "~0.6.0",

View File

@ -1,4 +1,4 @@
import { test } from '@playwright/test'
import { test, expect } from '@playwright/test'
import { openWorkbench } from './utils'
test.describe('contact tests', () => {
@ -22,4 +22,25 @@ test.describe('contact tests', () => {
await page.locator('.card-container').locator('button:has-text("Create")').click()
})
test('contact-search', async ({ page }) => {
// Create user and workspace
await openWorkbench(page)
await page.locator('[id="app-contact\\:string\\:Contacts"]').click()
await expect(page.locator('text=Elton John')).toBeVisible()
expect(await page.locator('.tr-body').count()).toBeGreaterThan(5)
const searchBox = page.locator('[placeholder="Search"]')
await searchBox.fill('Elton')
await searchBox.press('Enter')
await expect(page.locator('.tr-body')).toHaveCount(1)
await searchBox.fill('')
await searchBox.press('Enter')
await expect(page.locator('text=Rosamund Chen')).toBeVisible()
expect(await page.locator('.tr-body').count()).toBeGreaterThan(5)
})
})

View File

@ -0,0 +1,7 @@
import { PlaywrightTestConfig } from '@playwright/test'
const config: PlaywrightTestConfig = {
use: {
screenshot: 'only-on-failure'
}
}
export default config

View File

@ -95,4 +95,28 @@ test.describe('recruit tests', () => {
await expect(page.locator('text=John Multiseed').first()).toBeVisible()
await expect(page.locator('text=Alex P.').first()).toBeVisible()
})
test('application-search', async ({ page }) => {
// Create user and workspace
await openWorkbench(page)
await page.locator('[id="app-recruit\\:string\\:RecruitApplication"]').click()
await page.click('text=Software Engineer')
await expect(page.locator('text=Andrey P.')).toBeVisible()
expect(await page.locator('.tr-body').count()).toBeGreaterThan(2)
const searchBox = page.locator('[placeholder="Search"]')
await searchBox.fill('Frontend Engineer')
await searchBox.press('Enter')
await expect(page.locator('.tr-body')).toHaveCount(1)
await searchBox.fill('')
await searchBox.press('Enter')
await expect(page.locator('text=Andrey P.')).toBeVisible()
expect(await page.locator('.tr-body').count()).toBeGreaterThan(2)
})
})