Merge remote-tracking branch 'origin/develop' into staging

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-09-23 11:17:12 +07:00
commit 5e3e7f54f3
No known key found for this signature in database
GPG Key ID: BD80F68D68D8F7F2
20 changed files with 462 additions and 170 deletions

View File

@ -61,7 +61,6 @@
let levelReset: boolean = false let levelReset: boolean = false
let hovered: boolean = false let hovered: boolean = false
$: showArrow = selected && (type === 'type-link' || type === 'type-object')
$: if (!showMenu && levelReset && !hovered) levelReset = false $: if (!showMenu && levelReset && !hovered) levelReset = false
$: isOpen = !getTreeCollapsed(_id, collapsedPrefix) $: isOpen = !getTreeCollapsed(_id, collapsedPrefix)
$: setTreeCollapsed(_id, !isOpen, collapsedPrefix) $: setTreeCollapsed(_id, !isOpen, collapsedPrefix)
@ -147,14 +146,11 @@
</div> </div>
{/if} {/if}
{#if count !== null} {#if count !== null}
<span class="hulyNavItem-count font-regular-12"> <span class="hulyNavItem-count font-bold-12">
{count} {count}
</span> </span>
{/if} {/if}
<slot name="notify" /> <slot name="notify" />
{#if showArrow}
<div class="hulyNavItem-icon right"><IconOpenedArrow size={'small'} /></div>
{/if}
</button> </button>
{#if (isFold && (isOpen || (!isOpen && visible)) && !empty) || forciblyСollapsed} {#if (isFold && (isOpen || (!isOpen && visible)) && !empty) || forciblyСollapsed}
<div class="hulyNavItem-dropbox"> <div class="hulyNavItem-dropbox">
@ -202,6 +198,7 @@
} }
} }
.hulyNavItem-icon { .hulyNavItem-icon {
margin-right: var(--spacing-1);
width: var(--global-min-Size); width: var(--global-min-Size);
height: var(--global-min-Size); height: var(--global-min-Size);
color: var(--global-primary-TextColor); color: var(--global-primary-TextColor);
@ -212,14 +209,6 @@
height: 0.625rem; height: 0.625rem;
border-radius: var(--min-BorderRadius); border-radius: var(--min-BorderRadius);
} }
&.right {
visibility: hidden;
margin-left: var(--spacing-0_5);
color: var(--global-accent-IconColor);
}
&:not(.right) {
margin-right: var(--spacing-1);
}
&.withBackground { &.withBackground {
width: var(--global-extra-small-Size); width: var(--global-extra-small-Size);
height: var(--global-extra-small-Size); height: var(--global-extra-small-Size);
@ -245,12 +234,9 @@
gap: var(--spacing-0_25); gap: var(--spacing-0_25);
} }
.hulyNavItem-count { .hulyNavItem-count {
margin-left: var(--spacing-1); margin: 0 var(--spacing-1);
color: var(--global-tertiary-TextColor); color: var(--global-tertiary-TextColor);
} }
&:not(.selected) .hulyNavItem-count {
margin-right: var(--spacing-1);
}
&:not(.selected):hover, &:not(.selected):hover,
&:not(.selected).showMenu { &:not(.selected).showMenu {
background-color: var(--global-ui-hover-highlight-BackgroundColor); background-color: var(--global-ui-hover-highlight-BackgroundColor);
@ -259,27 +245,15 @@
cursor: default; cursor: default;
background-color: var(--global-ui-highlight-BackgroundColor); background-color: var(--global-ui-highlight-BackgroundColor);
// &:not(.type-anchor-link) .hulyNavItem-label:not(.description) {
// font-weight: 700;
// }
.hulyNavItem-actions {
order: 1;
margin-left: var(--spacing-0_5);
}
.hulyNavItem-count { .hulyNavItem-count {
color: var(--global-secondary-TextColor); color: var(--global-secondary-TextColor);
} }
} }
// &.bold:not(.type-anchor-link) .hulyNavItem-label:not(.description) {
// font-weight: 700;
// }
&.type-link { &.type-link {
padding: 0 var(--spacing-0_5) 0 var(--spacing-1_25); padding: 0 var(--spacing-0_5) 0 var(--spacing-1_25);
&.selected { &.selected {
padding: 0 var(--spacing-0_75) 0 var(--spacing-1_25);
&.indent { &.indent {
padding-left: var(--spacing-4); padding-left: var(--spacing-4);
} }
@ -289,9 +263,6 @@
.hulyNavItem-label:not(.description) { .hulyNavItem-label:not(.description) {
color: var(--global-accent-TextColor); color: var(--global-accent-TextColor);
} }
.hulyNavItem-icon.right {
visibility: visible;
}
} }
} }
&.type-tag { &.type-tag {
@ -303,28 +274,21 @@
} }
} }
&.type-object { &.type-object {
padding: 0 var(--spacing-0_5) 0 var(--spacing-0_5); padding: 0 var(--spacing-0_5);
.hulyNavItem-icon { .hulyNavItem-icon {
margin-right: var(--spacing-0_75);
width: var(--global-extra-small-Size); width: var(--global-extra-small-Size);
height: var(--global-extra-small-Size); height: var(--global-extra-small-Size);
&:not(.right) {
margin-right: var(--spacing-0_75);
background-color: var(--global-ui-BackgroundColor); background-color: var(--global-ui-BackgroundColor);
border-radius: var(--extra-small-BorderRadius); border-radius: var(--extra-small-BorderRadius);
} }
}
&.selected { &.selected {
.hulyNavItem-label:not(.description) { .hulyNavItem-label:not(.description) {
color: var(--global-accent-TextColor); color: var(--global-accent-TextColor);
} }
.hulyNavItem-icon { .hulyNavItem-icon {
color: var(--global-accent-TextColor); color: var(--global-accent-TextColor);
&.right {
visibility: visible;
}
} }
} }
} }
@ -353,15 +317,10 @@
background-color: var(--button-tertiary-hover-BackgroundColor); background-color: var(--button-tertiary-hover-BackgroundColor);
} }
&:not(.noActions):hover, &:not(.noActions):hover .hulyNavItem-actions,
&:not(.noActions).showMenu { &:not(.noActions).showMenu .hulyNavItem-actions {
.hulyNavItem-actions {
display: flex; display: flex;
} }
.hulyNavItem-icon.right {
display: none;
}
}
&.disabled { &.disabled {
cursor: not-allowed; cursor: not-allowed;

View File

@ -80,8 +80,11 @@
<div class="flex-column p-3 h-full" style:overflow="auto"> <div class="flex-column p-3 h-full" style:overflow="auto">
{#each Object.entries(activeSessions) as act} {#each Object.entries(activeSessions) as act}
{@const wsInstance = $workspacesStore.find((it) => it.workspaceId === act[0])} {@const wsInstance = $workspacesStore.find((it) => it.workspaceId === act[0])}
{@const totalFind = act[1].sessions.reduce((it, itm) => itm.current.find + it, 0)} {@const totalFind = act[1].sessions.reduce((it, itm) => itm.total.find + it, 0)}
{@const totalTx = act[1].sessions.reduce((it, itm) => itm.current.tx + it, 0)} {@const totalTx = act[1].sessions.reduce((it, itm) => itm.total.tx + it, 0)}
{@const currentFind = act[1].sessions.reduce((it, itm) => itm.current.find + it, 0)}
{@const currentTx = act[1].sessions.reduce((it, itm) => itm.current.tx + it, 0)}
{@const employeeGroups = Array.from(new Set(act[1].sessions.map((it) => it.userId))).filter( {@const employeeGroups = Array.from(new Set(act[1].sessions.map((it) => it.userId))).filter(
(it) => systemAccountEmail !== it || !realUsers (it) => systemAccountEmail !== it || !realUsers
)} )}
@ -94,7 +97,8 @@
<svelte:fragment slot="title"> <svelte:fragment slot="title">
<div class="flex flex-row-center flex-between flex-grow p-1"> <div class="flex flex-row-center flex-between flex-grow p-1">
<div class="fs-title" class:greyed={realGroup.length === 0}> <div class="fs-title" class:greyed={realGroup.length === 0}>
Workspace: {wsInstance?.workspaceName ?? act[0]}: {employeeGroups.length} current 5 mins => {totalFind}/{totalTx} Workspace: {wsInstance?.workspaceName ?? act[0]}: {employeeGroups.length} current 5 mins => {currentFind}/{currentTx},
total => {totalFind}/{totalTx}
{#if act[1].upgrading} {#if act[1].upgrading}
(Upgrading) (Upgrading)
{/if} {/if}
@ -138,7 +142,7 @@
{/if} {/if}
: {connections.length} : {connections.length}
<div class="ml-4"> <div class="ml-4">
<div class="ml-1">{find}/{txes}</div> <div class="ml-1">{find} rx/{txes} tx</div>
</div> </div>
</div> </div>
</svelte:fragment> </svelte:fragment>
@ -147,13 +151,13 @@
#{i} #{i}
{user.userId} {user.userId}
<div class="p-1"> <div class="p-1">
Total: {user.total.find}/{user.total.tx} Total: {user.total.find} rx/{user.total.tx} tx
</div> </div>
<div class="p-1"> <div class="p-1">
Previous 5 mins: {user.mins5.find}/{user.mins5.tx} Previous 5 mins: {user.mins5.find} rx/{user.mins5.tx} tx
</div> </div>
<div class="p-1"> <div class="p-1">
Current 5 mins: {user.current.find}/{user.current.tx} Current 5 mins: {user.current.find} tx/{user.current.tx} tx
</div> </div>
</div> </div>
<div class="p-1 flex-col ml-10"> <div class="p-1 flex-col ml-10">

View File

@ -1812,7 +1812,7 @@ export async function setRole (
}) })
} }
} finally { } finally {
if (client === undefined) { if (client == null) {
await connection.close() await connection.close()
} }
} }
@ -2049,7 +2049,7 @@ async function createPersonAccount (
} }
} }
} finally { } finally {
if (client === undefined) { if (client == null) {
await connection.close() await connection.close()
} }
} }

View File

@ -14,11 +14,12 @@
// limitations under the License. // limitations under the License.
// //
import { type Domain, type MeasureContext } from '@hcengineering/core' import { Analytics } from '@hcengineering/analytics'
import { DOMAIN_TX, type Domain, type MeasureContext } from '@hcengineering/core'
import { type DbAdapter, type DomainHelper } from './adapter' import { type DbAdapter, type DomainHelper } from './adapter'
import type { DbConfiguration } from './configuration'
import { DummyDbAdapter } from './mem' import { DummyDbAdapter } from './mem'
import type { DBAdapterManager, PipelineContext } from './types' import type { DBAdapterManager, PipelineContext } from './types'
import { Analytics } from '@hcengineering/analytics'
interface DomainInfo { interface DomainInfo {
exists: boolean exists: boolean
@ -35,7 +36,7 @@ export class DbAdapterManagerImpl implements DBAdapterManager {
constructor ( constructor (
private readonly metrics: MeasureContext, private readonly metrics: MeasureContext,
private readonly _domains: Record<string, string>, readonly conf: DbConfiguration,
private readonly context: PipelineContext, private readonly context: PipelineContext,
private readonly defaultAdapter: DbAdapter, private readonly defaultAdapter: DbAdapter,
private readonly adapters: Map<string, DbAdapter> private readonly adapters: Map<string, DbAdapter>
@ -86,8 +87,36 @@ export class DbAdapterManagerImpl implements DBAdapterManager {
} }
} }
async initAdapters (ctx: MeasureContext): Promise<void> {
await ctx.with('init-adapters', {}, async (ctx) => {
for (const [key, adapter] of this.adapters) {
// already initialized
if (key !== this.conf.domains[DOMAIN_TX] && adapter.init !== undefined) {
let excludeDomains: string[] | undefined
let domains: string[] | undefined
if (this.conf.defaultAdapter === key) {
excludeDomains = []
for (const domain in this.conf.domains) {
if (this.conf.domains[domain] !== key) {
excludeDomains.push(domain)
}
}
} else {
domains = []
for (const domain in this.conf.domains) {
if (this.conf.domains[domain] === key) {
domains.push(domain)
}
}
}
await adapter.init(domains, excludeDomains)
}
}
})
}
private async updateInfo (d: Domain, adapterDomains: Map<DbAdapter, Set<Domain>>, info: DomainInfo): Promise<void> { private async updateInfo (d: Domain, adapterDomains: Map<DbAdapter, Set<Domain>>, info: DomainInfo): Promise<void> {
const name = this._domains[d] ?? '#default' const name = this.conf.domains[d] ?? '#default'
const adapter = this.adapters.get(name) ?? this.defaultAdapter const adapter = this.adapters.get(name) ?? this.defaultAdapter
if (adapter !== undefined) { if (adapter !== undefined) {
const h = adapter.helper?.() const h = adapter.helper?.()
@ -129,7 +158,7 @@ export class DbAdapterManagerImpl implements DBAdapterManager {
} }
public getAdapter (domain: Domain, requireExists: boolean): DbAdapter { public getAdapter (domain: Domain, requireExists: boolean): DbAdapter {
const name = this._domains[domain] ?? '#default' const name = this.conf.domains[domain] ?? '#default'
const adapter = this.adapters.get(name) ?? this.defaultAdapter const adapter = this.adapters.get(name) ?? this.defaultAdapter
if (adapter === undefined) { if (adapter === undefined) {
throw new Error('adapter not provided: ' + name) throw new Error('adapter not provided: ' + name)

View File

@ -134,6 +134,8 @@ export interface DBAdapterManager {
close: () => Promise<void> close: () => Promise<void>
registerHelper: (helper: DomainHelper) => Promise<void> registerHelper: (helper: DomainHelper) => Promise<void>
initAdapters: (ctx: MeasureContext) => Promise<void>
} }
export interface PipelineContext { export interface PipelineContext {

View File

@ -77,41 +77,6 @@ export class DBAdapterMiddleware extends BaseMiddleware implements Middleware {
} }
} }
await txAdapter.init?.(txAdapterDomains) await txAdapter.init?.(txAdapterDomains)
const model = await txAdapter.getModel(ctx)
for (const tx of model) {
try {
this.context.hierarchy.tx(tx)
} catch (err: any) {
ctx.warn('failed to apply model transaction, skipping', { tx: JSON.stringify(tx), err })
}
}
await ctx.with('init-adapters', {}, async (ctx) => {
for (const [key, adapter] of adapters) {
// already initialized
if (key !== this.conf.domains[DOMAIN_TX] && adapter.init !== undefined) {
let excludeDomains: string[] | undefined
let domains: string[] | undefined
if (this.conf.defaultAdapter === key) {
excludeDomains = []
for (const domain in this.conf.domains) {
if (this.conf.domains[domain] !== key) {
excludeDomains.push(domain)
}
}
} else {
domains = []
for (const domain in this.conf.domains) {
if (this.conf.domains[domain] === key) {
domains.push(domain)
}
}
}
await adapter.init(domains, excludeDomains)
}
}
})
const metrics = this.conf.metrics.newChild('📔 server-storage', {}) const metrics = this.conf.metrics.newChild('📔 server-storage', {})
@ -127,7 +92,7 @@ export class DBAdapterMiddleware extends BaseMiddleware implements Middleware {
// We need to init all next, since we will use model // We need to init all next, since we will use model
const adapterManager = new DbAdapterManagerImpl(metrics, this.conf.domains, this.context, defaultAdapter, adapters) const adapterManager = new DbAdapterManagerImpl(metrics, this.conf, this.context, defaultAdapter, adapters)
this.context.adapterManager = adapterManager this.context.adapterManager = adapterManager
} }

View File

@ -20,12 +20,13 @@ import { BaseMiddleware, DomainIndexHelperImpl } from '@hcengineering/server-cor
/** /**
* @public * @public
*/ */
export class DBAdapterHelperMiddleware extends BaseMiddleware implements Middleware { export class DBAdapterInitMiddleware extends BaseMiddleware implements Middleware {
static async create ( static async create (
ctx: MeasureContext, ctx: MeasureContext,
context: PipelineContext, context: PipelineContext,
next?: Middleware next?: Middleware
): Promise<Middleware | undefined> { ): Promise<Middleware | undefined> {
await context.adapterManager?.initAdapters?.(ctx)
const domainHelper = new DomainIndexHelperImpl(ctx, context.hierarchy, context.modelDb, context.workspace) const domainHelper = new DomainIndexHelperImpl(ctx, context.hierarchy, context.modelDb, context.workspace)
await context.adapterManager?.registerHelper?.(domainHelper) await context.adapterManager?.registerHelper?.(domainHelper)
return undefined return undefined

View File

@ -312,7 +312,9 @@ abstract class MongoAdapterBase implements DbAdapter {
} }
const baseClass = this.hierarchy.getBaseClass(clazz) const baseClass = this.hierarchy.getBaseClass(clazz)
if (baseClass !== core.class.Doc) { if (baseClass !== core.class.Doc) {
const classes = this.hierarchy.getDescendants(baseClass).filter((it) => !this.hierarchy.isMixin(it)) const classes = Array.from(
new Set(this.hierarchy.getDescendants(baseClass).filter((it) => !this.hierarchy.isMixin(it)))
)
// Only replace if not specified // Only replace if not specified
if (translatedBase._class === undefined) { if (translatedBase._class === undefined) {
@ -334,7 +336,9 @@ abstract class MongoAdapterBase implements DbAdapter {
descendants = descendants.filter((c) => !excludedClassesIds.has(c)) descendants = descendants.filter((c) => !excludedClassesIds.has(c))
} }
const desc = descendants.filter((it: any) => !this.hierarchy.isMixin(it as Ref<Class<Doc>>)) const desc = Array.from(
new Set(descendants.filter((it: any) => !this.hierarchy.isMixin(it as Ref<Class<Doc>>)))
)
translatedBase._class = desc.length === 1 ? desc[0] : { $in: desc } translatedBase._class = desc.length === 1 ? desc[0] : { $in: desc }
} }
@ -348,6 +352,9 @@ abstract class MongoAdapterBase implements DbAdapter {
delete translatedBase._class delete translatedBase._class
} }
} }
if (translatedBase._class?.$in != null && Array.isArray(translatedBase._class.$in)) {
translatedBase._class.$in = Array.from(new Set(translatedBase._class.$in))
}
if (translatedBase._class?.$in?.length === 1 && translatedBase._class?.$nin === undefined) { if (translatedBase._class?.$in?.length === 1 && translatedBase._class?.$nin === undefined) {
translatedBase._class = translatedBase._class.$in[0] translatedBase._class = translatedBase._class.$in[0]
} }

View File

@ -18,7 +18,7 @@ import {
BroadcastMiddleware, BroadcastMiddleware,
ConfigurationMiddleware, ConfigurationMiddleware,
ContextNameMiddleware, ContextNameMiddleware,
DBAdapterHelperMiddleware, DBAdapterInitMiddleware,
DBAdapterMiddleware, DBAdapterMiddleware,
DomainFindMiddleware, DomainFindMiddleware,
DomainTxMiddleware, DomainTxMiddleware,
@ -135,7 +135,7 @@ export function createServerPipeline (
LiveQueryMiddleware.create, LiveQueryMiddleware.create,
DomainFindMiddleware.create, DomainFindMiddleware.create,
DomainTxMiddleware.create, DomainTxMiddleware.create,
DBAdapterHelperMiddleware.create, DBAdapterInitMiddleware.create,
ModelMiddleware.create, ModelMiddleware.create,
DBAdapterMiddleware.create(conf), // Configure DB adapters DBAdapterMiddleware.create(conf), // Configure DB adapters
BroadcastMiddleware.create(broadcast) BroadcastMiddleware.create(broadcast)

View File

@ -48,6 +48,9 @@ import {
} from './types' } from './types'
import { sendResponse } from './utils' import { sendResponse } from './utils'
const ticksPerSecond = 20
const workspaceSoftShutdownTicks = 3 * ticksPerSecond
interface WorkspaceLoginInfo extends Omit<BaseWorkspaceInfo, 'workspace'> { interface WorkspaceLoginInfo extends Omit<BaseWorkspaceInfo, 'workspace'> {
upgrade?: { upgrade?: {
toProcess: number toProcess: number
@ -75,7 +78,7 @@ function timeoutPromise (time: number): { promise: Promise<void>, cancelHandle:
*/ */
export interface Timeouts { export interface Timeouts {
// Timeout preferences // Timeout preferences
pingTimeout: number // Default 1 second pingTimeout: number // Default 10 second
reconnectTimeout: number // Default 3 seconds reconnectTimeout: number // Default 3 seconds
} }
@ -113,8 +116,8 @@ class TSessionManager implements SessionManager {
} }
) { ) {
this.checkInterval = setInterval(() => { this.checkInterval = setInterval(() => {
this.handleInterval() this.handleTick()
}, timeouts.pingTimeout) }, 1000 / ticksPerSecond)
} }
scheduleMaintenance (timeMinutes: number): void { scheduleMaintenance (timeMinutes: number): void {
@ -168,34 +171,44 @@ class TSessionManager implements SessionManager {
ticks = 0 ticks = 0
handleInterval (): void { handleTick (): void {
for (const [wsId, workspace] of this.workspaces.entries()) { for (const [wsId, workspace] of this.workspaces.entries()) {
for (const s of workspace.sessions) { for (const s of workspace.sessions) {
if (this.ticks % (5 * 60) === 0) { if (this.ticks % (5 * 60 * ticksPerSecond) === 0) {
s[1].session.mins5.find = s[1].session.current.find s[1].session.mins5.find = s[1].session.current.find
s[1].session.mins5.tx = s[1].session.current.tx s[1].session.mins5.tx = s[1].session.current.tx
s[1].session.current = { find: 0, tx: 0 } s[1].session.current = { find: 0, tx: 0 }
} }
const now = Date.now() const now = Date.now()
const diff = now - s[1].session.lastRequest const lastRequestDiff = now - s[1].session.lastRequest
let timeout = 60000 let timeout = 60000
if (s[1].session.getUser() === systemAccountEmail) { if (s[1].session.getUser() === systemAccountEmail) {
timeout = timeout * 10 timeout = timeout * 10
} }
if (diff > timeout && this.ticks % 10 === 0) { const isCurrentUserTick = this.ticks % ticksPerSecond === s[1].tickHash
if (isCurrentUserTick) {
if (lastRequestDiff > timeout) {
this.ctx.warn('session hang, closing...', { wsId, user: s[1].session.getUser() }) this.ctx.warn('session hang, closing...', { wsId, user: s[1].session.getUser() })
// Force close workspace if only one client and it hang. // Force close workspace if only one client and it hang.
void this.close(this.ctx, s[1].socket, wsId) void this.close(this.ctx, s[1].socket, wsId)
continue continue
} }
if (diff > 20000 && diff < 60000 && this.ticks % 10 === 0) { if (lastRequestDiff + (1 / 10) * lastRequestDiff > this.timeouts.pingTimeout) {
s[1].socket.send(workspace.context, { result: 'ping' }, s[1].session.binaryMode, s[1].session.useCompression) // We need to check state and close socket if it broken
if (s[1].socket.checkState()) {
s[1].socket.send(
workspace.context,
{ result: 'ping' },
s[1].session.binaryMode,
s[1].session.useCompression
)
}
} }
for (const r of s[1].session.requests.values()) { for (const r of s[1].session.requests.values()) {
if (now - r.start > 30000) { if (now - r.start > 30000) {
this.ctx.warn('request hang found, 30sec', { this.ctx.warn('request hang found, 30sec', {
@ -206,9 +219,10 @@ class TSessionManager implements SessionManager {
} }
} }
} }
}
// Wait some time for new client to appear before closing workspace. // Wait some time for new client to appear before closing workspace.
if (workspace.sessions.size === 0 && workspace.closing === undefined) { if (workspace.sessions.size === 0 && workspace.closing === undefined && workspace.workspaceInitCompleted) {
workspace.softShutdown-- workspace.softShutdown--
if (workspace.softShutdown <= 0) { if (workspace.softShutdown <= 0) {
this.ctx.warn('closing workspace, no users', { this.ctx.warn('closing workspace, no users', {
@ -219,7 +233,7 @@ class TSessionManager implements SessionManager {
workspace.closing = this.performWorkspaceCloseCheck(workspace, workspace.workspaceId, wsId) workspace.closing = this.performWorkspaceCloseCheck(workspace, workspace.workspaceId, wsId)
} }
} else { } else {
workspace.softShutdown = 3 workspace.softShutdown = workspaceSoftShutdownTicks
} }
if (this.clientErrors !== this.oldClientErrors) { if (this.clientErrors !== this.oldClientErrors) {
@ -270,6 +284,8 @@ class TSessionManager implements SessionManager {
} }
} }
sessionCounter = 0
@withContext('📲 add-session') @withContext('📲 add-session')
async addSession ( async addSession (
ctx: MeasureContext, ctx: MeasureContext,
@ -423,7 +439,13 @@ class TSessionManager implements SessionManager {
session.sessionInstanceId = generateId() session.sessionInstanceId = generateId()
this.sessions.set(ws.id, { session, socket: ws }) this.sessions.set(ws.id, { session, socket: ws })
// We need to delete previous session with Id if found. // We need to delete previous session with Id if found.
workspace.sessions.set(session.sessionId, { session, socket: ws }) this.sessionCounter++
workspace.sessions.set(session.sessionId, { session, socket: ws, tickHash: this.sessionCounter % ticksPerSecond })
// Mark workspace as init completed and we had at least one client.
if (!workspace.workspaceInitCompleted) {
workspace.workspaceInitCompleted = true
}
// We do not need to wait for set-status, just return session to client // We do not need to wait for set-status, just return session to client
const _workspace = workspace const _workspace = workspace
@ -593,11 +615,12 @@ class TSessionManager implements SessionManager {
branding branding
), ),
sessions: new Map(), sessions: new Map(),
softShutdown: 3, softShutdown: workspaceSoftShutdownTicks,
upgrade, upgrade,
workspaceId: token.workspace, workspaceId: token.workspace,
workspaceName, workspaceName,
branding branding,
workspaceInitCompleted: false
} }
this.workspaces.set(toWorkspaceString(token.workspace), workspace) this.workspaces.set(toWorkspaceString(token.workspace), workspace)
return workspace return workspace

View File

@ -93,6 +93,8 @@ export interface ConnectionSocket {
data: () => Record<string, any> data: () => Record<string, any>
readRequest: (buffer: Buffer, binary: boolean) => Request<any> readRequest: (buffer: Buffer, binary: boolean) => Request<any>
checkState: () => boolean
} }
/** /**
@ -114,11 +116,12 @@ export interface Workspace {
context: MeasureContext context: MeasureContext
id: string id: string
pipeline: Promise<Pipeline> pipeline: Promise<Pipeline>
sessions: Map<string, { session: Session, socket: ConnectionSocket }> sessions: Map<string, { session: Session, socket: ConnectionSocket, tickHash: number }>
upgrade: boolean upgrade: boolean
closing?: Promise<void> closing?: Promise<void>
softShutdown: number softShutdown: number
workspaceInitCompleted: boolean
workspaceId: WorkspaceId workspaceId: WorkspaceId
workspaceName: string workspaceName: string

View File

@ -310,7 +310,9 @@ export function startHttpServer (
} }
: false, : false,
skipUTF8Validation: true, skipUTF8Validation: true,
maxPayload: 250 * 1024 * 1024 maxPayload: 250 * 1024 * 1024,
backlog: 1000,
clientTracking: false // We do not need to track clients inside clients.
}) })
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
const handleConnection = async ( const handleConnection = async (
@ -492,6 +494,14 @@ function createWebsocketClientSocket (
close: () => { close: () => {
cs.isClosed = true cs.isClosed = true
ws.close() ws.close()
ws.terminate()
},
checkState: () => {
if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
ws.terminate()
return false
}
return true
}, },
readRequest: (buffer: Buffer, binary: boolean) => { readRequest: (buffer: Buffer, binary: boolean) => {
return rpcHandler.readRequest(buffer, binary) return rpcHandler.readRequest(buffer, binary)

View File

@ -6,6 +6,7 @@ import { SignUpData } from '../model/common-types'
import { LeftSideMenuPage } from '../model/left-side-menu-page' import { LeftSideMenuPage } from '../model/left-side-menu-page'
import { LoginPage } from '../model/login-page' import { LoginPage } from '../model/login-page'
import { SelectWorkspacePage } from '../model/select-workspace-page' import { SelectWorkspacePage } from '../model/select-workspace-page'
import { SidebarPage } from '../model/sidebar-page'
import { import {
PlatformURI, PlatformURI,
generateTestData, generateTestData,
@ -19,6 +20,7 @@ test.describe('Channel tests', () => {
let leftSideMenuPage: LeftSideMenuPage let leftSideMenuPage: LeftSideMenuPage
let chunterPage: ChunterPage let chunterPage: ChunterPage
let channelPage: ChannelPage let channelPage: ChannelPage
let sidebarPage: SidebarPage
let loginPage: LoginPage let loginPage: LoginPage
let api: ApiEndpoint let api: ApiEndpoint
let newUser2: SignUpData let newUser2: SignUpData
@ -32,6 +34,7 @@ test.describe('Channel tests', () => {
chunterPage = new ChunterPage(page) chunterPage = new ChunterPage(page)
channelPage = new ChannelPage(page) channelPage = new ChannelPage(page)
loginPage = new LoginPage(page) loginPage = new LoginPage(page)
sidebarPage = new SidebarPage(page)
api = new ApiEndpoint(request) api = new ApiEndpoint(request)
await api.createAccount(data.userName, '1234', data.firstName, data.lastName) await api.createAccount(data.userName, '1234', data.firstName, data.lastName)
await api.createWorkspaceWithLogin(data.workspaceName, data.userName, '1234') await api.createWorkspaceWithLogin(data.workspaceName, data.userName, '1234')
@ -395,7 +398,7 @@ test.describe('Channel tests', () => {
await channelPageSecond.checkMessageExist(`@${mentionName}`, true, `@${mentionName}`) await channelPageSecond.checkMessageExist(`@${mentionName}`, true, `@${mentionName}`)
}) })
test('User can star and unstar a channel', async () => { test('User is able to star and unstar a channel', async () => {
await test.step('Prepare channel', async () => { await test.step('Prepare channel', async () => {
await leftSideMenuPage.clickChunter() await leftSideMenuPage.clickChunter()
await chunterPage.clickChannelBrowser() await chunterPage.clickChannelBrowser()
@ -419,7 +422,7 @@ test.describe('Channel tests', () => {
}) })
}) })
test('User can leave and join a channel', async () => { test('User is able to leave and join a channel', async () => {
await test.step('Prepare channel', async () => { await test.step('Prepare channel', async () => {
await leftSideMenuPage.clickChunter() await leftSideMenuPage.clickChunter()
await chunterPage.clickChannelBrowser() await chunterPage.clickChannelBrowser()
@ -461,7 +464,7 @@ test.describe('Channel tests', () => {
}) })
}) })
test('User can filter channels in table', async () => { test('User is able to filter channels in table', async () => {
await test.step('Prepare channel', async () => { await test.step('Prepare channel', async () => {
await leftSideMenuPage.clickChunter() await leftSideMenuPage.clickChunter()
await chunterPage.clickChannelBrowser() await chunterPage.clickChannelBrowser()
@ -486,7 +489,7 @@ test.describe('Channel tests', () => {
}) })
}) })
test('User can search channel in table', async () => { test('User is able to search channel in table', async () => {
await test.step('Prepare channel', async () => { await test.step('Prepare channel', async () => {
await leftSideMenuPage.clickChunter() await leftSideMenuPage.clickChunter()
await chunterPage.clickChannelBrowser() await chunterPage.clickChannelBrowser()
@ -512,4 +515,110 @@ test.describe('Channel tests', () => {
await channelPage.checkIfChannelTableExist('general', true) await channelPage.checkIfChannelTableExist('general', true)
}) })
}) })
test('User is able to work with a channel in a sidebar', async () => {
await test.step('Prepare channel', async () => {
await leftSideMenuPage.clickChunter()
await chunterPage.clickChannelBrowser()
await chunterPage.clickNewChannelHeader()
await chunterPage.createPrivateChannel(data.channelName, false)
await channelPage.checkIfChannelDefaultExist(true, data.channelName)
await leftSideMenuPage.clickChunter()
await channelPage.clickChooseChannel(data.channelName)
await channelPage.sendMessage('Test message')
})
await test.step('Open channel in sidebar', async () => {
await sidebarPage.checkIfSidebarPageButtonIsExist(false, 'chat')
await channelPage.makeActionWithChannelInMenu(data.channelName, 'Open in sidebar')
await sidebarPage.checkIfSidebarPageButtonIsExist(true, 'chat')
await sidebarPage.checkIfSidebarHasVerticalTab(true, data.channelName)
await sidebarPage.checkIfSidebarIsOpen(true)
await sidebarPage.checkIfChatSidebarTabIsOpen(true, data.channelName)
})
await test.step('Go to another page and check if sidebar will be keeping', async () => {
await leftSideMenuPage.clickTracker()
await sidebarPage.checkIfSidebarIsOpen(true)
await sidebarPage.checkIfChatSidebarTabIsOpen(true, data.channelName)
await leftSideMenuPage.clickChunter()
})
await test.step('Close channel in sidebar', async () => {
await sidebarPage.closeOpenedVerticalTab()
await sidebarPage.checkIfSidebarHasVerticalTab(false, data.channelName)
await sidebarPage.checkIfSidebarIsOpen(false)
})
await test.step('Reopen channel in sidebar', async () => {
await channelPage.makeActionWithChannelInMenu(data.channelName, 'Open in sidebar')
await sidebarPage.checkIfSidebarHasVerticalTab(true, data.channelName)
await sidebarPage.checkIfChatSidebarTabIsOpen(true, data.channelName)
})
await test.step('Open general in sidebar too', async () => {
await channelPage.makeActionWithChannelInMenu('general', 'Open in sidebar')
await sidebarPage.checkIfSidebarHasVerticalTab(true, data.channelName)
await sidebarPage.checkIfSidebarHasVerticalTab(true, 'general')
await sidebarPage.checkIfChatSidebarTabIsOpen(true, 'general')
})
await test.step('Pin and unpin channel tab', async () => {
await sidebarPage.pinVerticalTab(data.channelName)
await sidebarPage.checkIfVerticalTabIsPinned(true, data.channelName)
await sidebarPage.unpinVerticalTab(data.channelName)
await sidebarPage.checkIfVerticalTabIsPinned(false, data.channelName)
})
await test.step('Close sidebar tab by close button in vertical tab', async () => {
await sidebarPage.clickVerticalTab(data.channelName)
await sidebarPage.closeVerticalTabByCloseButton(data.channelName)
await sidebarPage.checkIfSidebarHasVerticalTab(false, data.channelName)
await sidebarPage.checkIfChatSidebarTabIsOpen(true, 'general')
})
await test.step('Close sidebar tab by context menu', async () => {
await channelPage.makeActionWithChannelInMenu('random', 'Open in sidebar')
await sidebarPage.closeVerticalTabByRightClick('random')
await sidebarPage.checkIfSidebarHasVerticalTab(false, 'random')
await sidebarPage.checkIfChatSidebarTabIsOpen(true, 'general')
})
await test.step('Close the last channel tab in Sidebar', async () => {
await sidebarPage.closeVerticalTabByCloseButton('general')
await sidebarPage.checkIfSidebarIsOpen(false)
await sidebarPage.checkIfSidebarPageButtonIsExist(false, 'chat')
})
})
test('User is able to create thread automatically in Sidebar', async () => {
await test.step('Prepare channel', async () => {
await leftSideMenuPage.clickChunter()
await chunterPage.clickChannelBrowser()
await chunterPage.clickNewChannelHeader()
await chunterPage.createPrivateChannel(data.channelName, false)
await channelPage.checkIfChannelDefaultExist(true, data.channelName)
await leftSideMenuPage.clickChunter()
await channelPage.clickChooseChannel(data.channelName)
await channelPage.sendMessage('Test message')
})
await test.step('Open channel in Sidebar', async () => {
await channelPage.replyToMessage('Test message', 'Reply message')
await sidebarPage.checkIfSidebarIsOpen(true)
await sidebarPage.checkIfSidebarHasVerticalTab(true, data.channelName)
await sidebarPage.checkIfChatSidebarTabIsOpen(true, 'Thread')
await sidebarPage.checkIfChatSidebarTabIsOpen(true, data.channelName)
})
await test.step('User go to another chat and Sidebar with tread disappears', async () => {
await channelPage.clickChannel('random')
await sidebarPage.checkIfSidebarIsOpen(false)
})
})
}) })

View File

@ -151,7 +151,7 @@ test.describe('Content in the Documents tests', () => {
}) })
test.describe('Image in the document', () => { test.describe('Image in the document', () => {
test('Check Image alignment setting', async ({ page }) => { test('Check image alignment setting', async ({ page }) => {
await documentContentPage.addImageToDocument(page) await documentContentPage.addImageToDocument(page)
await test.step('Align image to right', async () => { await test.step('Align image to right', async () => {
@ -170,9 +170,8 @@ test.describe('Content in the Documents tests', () => {
}) })
}) })
test.skip('Check Image view and size actions', async ({ page }) => { test.skip('Check Image size manipulations', async ({ page }) => {
await documentContentPage.addImageToDocument(page) await documentContentPage.addImageToDocument(page)
const imageSrc = await documentContentPage.firstImageInDocument().getAttribute('src')
await test.step('Set size of image to the 25%', async () => { await test.step('Set size of image to the 25%', async () => {
await documentContentPage.clickImageSizeButton('25%') await documentContentPage.clickImageSizeButton('25%')
@ -194,6 +193,11 @@ test.describe('Content in the Documents tests', () => {
await documentContentPage.clickImageSizeButton('Unset') await documentContentPage.clickImageSizeButton('Unset')
await documentContentPage.checkImageSize(IMAGE_ORIGINAL_SIZE) await documentContentPage.checkImageSize(IMAGE_ORIGINAL_SIZE)
}) })
})
test('Check Image views', async ({ page, context }) => {
await documentContentPage.addImageToDocument(page)
const imageSrc = await documentContentPage.firstImageInDocument().getAttribute('src')
await test.step('User can open image in fullscreen on current page', async () => { await test.step('User can open image in fullscreen on current page', async () => {
await documentContentPage.clickImageFullscreenButton() await documentContentPage.clickImageFullscreenButton()
@ -203,12 +207,11 @@ test.describe('Content in the Documents tests', () => {
}) })
await test.step('User can open image original in the new tab', async () => { await test.step('User can open image original in the new tab', async () => {
const [newPage] = await Promise.all([ const pagePromise = context.waitForEvent('page')
page.waitForEvent('popup'), await documentContentPage.clickImageOriginalButton()
documentContentPage.clickImageOriginalButton() const newPage = await pagePromise
])
await newPage.waitForLoadState('domcontentloaded') await newPage.waitForLoadState()
expect(newPage.url()).toBe(imageSrc) expect(newPage.url()).toBe(imageSrc)
await newPage.close() await newPage.close()
}) })

View File

@ -16,7 +16,7 @@ export class CommonPage {
selectPopupButton = (): Locator => this.page.locator('div.selectPopup button') selectPopupButton = (): Locator => this.page.locator('div.selectPopup button')
selectPopupExpandButton = (): Locator => this.page.locator('div.selectPopup button[data-id="btnExpand"]') selectPopupExpandButton = (): Locator => this.page.locator('div.selectPopup button[data-id="btnExpand"]')
popupSpanLabel = (point: string): Locator => popupSpanLabel = (point: string): Locator =>
this.page.locator('div[class$="opup"] span[class*="label"]', { hasText: point }) this.page.locator(`div[class$="opup"] span[class*="label"]:has-text("${point}")`)
readonly inputSearchIcon = (): Locator => this.page.locator('.searchInput-icon') readonly inputSearchIcon = (): Locator => this.page.locator('.searchInput-icon')

View File

@ -16,6 +16,7 @@ export class PlanningPage extends CalendarPage {
private readonly panel = (): Locator => this.page.locator('div.hulyModal-container') private readonly panel = (): Locator => this.page.locator('div.hulyModal-container')
private readonly toDosContainer = (): Locator => this.page.locator('div.toDos-container') private readonly toDosContainer = (): Locator => this.page.locator('div.toDos-container')
private readonly schedule = (): Locator => this.page.locator('div.hulyComponent.modal') private readonly schedule = (): Locator => this.page.locator('div.hulyComponent.modal')
private readonly sidebarSchedule = (): Locator => this.page.locator('#sidebar .calendar-container')
readonly pageHeader = (): Locator => readonly pageHeader = (): Locator =>
this.page.locator('div[class*="navigator"] div[class*="header"]', { hasText: 'Planning' }) this.page.locator('div[class*="navigator"] div[class*="header"]', { hasText: 'Planning' })
@ -81,6 +82,9 @@ export class PlanningPage extends CalendarPage {
readonly eventInSchedule = (title: string): Locator => readonly eventInSchedule = (title: string): Locator =>
this.schedule().locator('div.event-container', { hasText: title }) this.schedule().locator('div.event-container', { hasText: title })
readonly eventInSidebarSchedule = (title: string): Locator =>
this.sidebarSchedule().locator('div.event-container', { hasText: title })
readonly toDoInToDos = (hasText: string): Locator => readonly toDoInToDos = (hasText: string): Locator =>
this.toDosContainer().locator('button.hulyToDoLine-container', { hasText }) this.toDosContainer().locator('button.hulyToDoLine-container', { hasText })

View File

@ -19,12 +19,13 @@ export class TalentDetailsPage extends CommonRecruitingPage {
readonly buttonMergeContacts = (): Locator => readonly buttonMergeContacts = (): Locator =>
this.page.locator('button[class*="menuItem"] span', { hasText: 'Merge contacts' }) this.page.locator('button[class*="menuItem"] span', { hasText: 'Merge contacts' })
readonly buttonFinalContact = (): Locator => readonly formMergeContacts = (): Locator => this.page.locator('form[id="contact:string:MergePersons"]')
this.page.locator('form[id="contact:string:MergePersons"] button', { hasText: 'Final contact' })
readonly buttonMergeRow = (): Locator => this.page.locator('form[id="contact:string:MergePersons"] div.box') readonly buttonFinalContact = (): Locator => this.formMergeContacts().locator('button', { hasText: 'Final contact' })
readonly buttonMergeRow = (): Locator => this.formMergeContacts().locator('div.box')
readonly buttonPopupMergeContacts = (): Locator => readonly buttonPopupMergeContacts = (): Locator =>
this.page.locator('form[id="contact:string:MergePersons"] button > span', { hasText: 'Merge contacts' }) this.formMergeContacts().locator('button:has-text("Merge contacts")')
readonly textAttachmentName = (): Locator => this.page.locator('div.name a') readonly textAttachmentName = (): Locator => this.page.locator('div.name a')
readonly titleAndSourceTalent = (title: string): Locator => this.page.locator('button > span', { hasText: title }) readonly titleAndSourceTalent = (title: string): Locator => this.page.locator('button > span', { hasText: title })
@ -42,6 +43,12 @@ export class TalentDetailsPage extends CommonRecruitingPage {
await expect(this.textTagItem().first()).toContainText(skillTag) await expect(this.textTagItem().first()).toContainText(skillTag)
} }
async enterLocation (location: string): Promise<void> {
const input = this.inputLocation()
await input.click()
await input.fill(location)
}
async addTitle (title: string): Promise<void> { async addTitle (title: string): Promise<void> {
await this.buttonInputTitle().click() await this.buttonInputTitle().click()
await this.fillToSelectPopup(this.page, title) await this.fillToSelectPopup(this.page, title)
@ -97,8 +104,8 @@ export class TalentDetailsPage extends CommonRecruitingPage {
} }
} }
async checkMergeContacts (talentName: string, title: string, source: string): Promise<void> { async checkMergeContacts (location: string, title: string, source: string): Promise<void> {
await expect(this.page.locator('div.location input')).toHaveValue(talentName) await expect(this.page.locator('div.location input')).toHaveValue(location)
await expect(this.titleAndSourceTalent(title)).toBeVisible() await expect(this.titleAndSourceTalent(title)).toBeVisible()
await expect(this.titleAndSourceTalent(source)).toBeVisible() await expect(this.titleAndSourceTalent(source)).toBeVisible()
} }

View File

@ -0,0 +1,120 @@
import { expect, Locator, Page } from '@playwright/test'
import { CommonPage } from './common-page'
export type SidebarTabTypes = 'calendar' | 'office' | 'chat'
export class SidebarPage extends CommonPage {
readonly page: Page
constructor (page: Page) {
super(page)
this.page = page
}
sidebar = (): Locator => this.page.locator('#sidebar')
content = (): Locator => this.sidebar().locator('.content')
contentHeaderByTitle = (title: string): Locator =>
this.content().locator(`.hulyHeader-titleGroup:has-text("${title}")`)
contentCloseButton = (): Locator => this.content().locator('.hulyHeader-container button.iconOnly').last()
calendarSidebarButton = (): Locator => this.sidebar().locator('button[id$="Calendar"]')
officeSidebarButton = (): Locator => this.sidebar().locator('button[id$="Office"]')
chatSidebarButton = (): Locator => this.sidebar().locator('button[id$="Chat"]')
verticalTabs = (): Locator => this.sidebar().locator('.tabs').locator('.container')
verticalTabByName = (name: string): Locator =>
this.sidebar().locator('.tabs').locator(`.container:has-text("${name}")`)
verticalTabCloseButton = (name: string): Locator => this.verticalTabByName(name).locator('.close-button button')
currentYear = new Date().getFullYear().toString()
plannerSidebarNextDayButton = (): Locator =>
this.sidebar().locator('.hulyHeader-buttonsGroup').getByRole('button').last()
// buttonOpenChannelInSidebar =
// Actions
async checkIfSidebarIsOpen (isOpen: boolean): Promise<void> {
await expect(this.content()).toBeVisible({ visible: isOpen })
}
async checkIfSidebarHasVerticalTab (isExist: boolean, tabName: string): Promise<void> {
await expect(this.verticalTabByName(tabName)).toBeVisible({ visible: isExist })
}
async clickVerticalTab (tabName: string): Promise<void> {
await this.verticalTabByName(tabName).click()
}
async closeVerticalTabByCloseButton (tabName: string): Promise<void> {
await this.verticalTabCloseButton(tabName).click()
}
async closeOpenedVerticalTab (): Promise<void> {
await this.contentCloseButton().click()
}
async pinVerticalTab (tabName: string): Promise<void> {
await this.verticalTabByName(tabName).click({ button: 'right' })
await this.page.locator('.popup').locator('button:has-text("Pin")').click()
}
async unpinVerticalTab (tabName: string): Promise<void> {
await this.verticalTabByName(tabName).click({ button: 'right' })
await this.page.locator('.popup').locator('button:has-text("Unpin")').click()
}
async closeVerticalTabByRightClick (tabName: string): Promise<void> {
await this.verticalTabByName(tabName).click({ button: 'right' })
await this.page.locator('.popup').locator('button:has-text("Close")').click()
}
async checkIfVerticalTabIsPinned (needBePinned: boolean, tabName: string): Promise<void> {
await expect(this.verticalTabCloseButton(tabName)).toBeVisible({ visible: !needBePinned })
}
async checkNumberOfVerticalTabs (count: number): Promise<void> {
await expect(this.verticalTabs()).toHaveCount(count)
}
async checkIfPlanerSidebarTabIsOpen (isExist: boolean): Promise<void> {
await expect(this.contentHeaderByTitle(this.currentYear)).toBeVisible({ visible: isExist })
}
async checkIfChatSidebarTabIsOpen (isExist: boolean, channelName: string): Promise<void> {
await expect(this.contentHeaderByTitle(channelName)).toBeVisible({ visible: isExist })
}
async checkIfOfficeSidebarTabIsOpen (isExist: boolean, channelName: string): Promise<void> {
await expect(this.contentHeaderByTitle('Office')).toBeVisible({ visible: isExist })
}
async checkIfSidebarPageButtonIsExist (isExist: boolean, type: SidebarTabTypes): Promise<void> {
switch (type) {
case 'chat':
await expect(this.chatSidebarButton()).toBeVisible({ visible: isExist })
break
case 'office':
await expect(this.officeSidebarButton()).toBeVisible({ visible: isExist })
break
case 'calendar':
await expect(this.calendarSidebarButton()).toBeVisible({ visible: isExist })
break
}
}
async clickSidebarPageButton (type: SidebarTabTypes): Promise<void> {
switch (type) {
case 'chat':
await this.chatSidebarButton().click()
break
case 'office':
await this.officeSidebarButton().click()
break
case 'calendar':
await this.calendarSidebarButton().click()
break
}
}
}

View File

@ -1,5 +1,13 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { generateId, PlatformSetting, PlatformURI, generateTestData, getTimeForPlanner } from '../utils' import {
generateId,
PlatformSetting,
PlatformURI,
generateTestData,
getTimeForPlanner,
getSecondPageByInvite,
getInviteLink
} from '../utils'
import { PlanningPage } from '../model/planning/planning-page' import { PlanningPage } from '../model/planning/planning-page'
import { NewToDo } from '../model/planning/types' import { NewToDo } from '../model/planning/types'
import { PlanningNavigationMenuPage } from '../model/planning/planning-navigation-menu-page' import { PlanningNavigationMenuPage } from '../model/planning/planning-navigation-menu-page'
@ -9,9 +17,10 @@ import { faker } from '@faker-js/faker'
import { LeftSideMenuPage } from '../model/left-side-menu-page' import { LeftSideMenuPage } from '../model/left-side-menu-page'
import { ApiEndpoint } from '../API/Api' import { ApiEndpoint } from '../API/Api'
import { LoginPage } from '../model/login-page' import { LoginPage } from '../model/login-page'
import { SignInJoinPage } from '../model/signin-page' import { SidebarPage } from '../model/sidebar-page'
import { TeamPage } from '../model/team-page' import { TeamPage } from '../model/team-page'
import { SelectWorkspacePage } from '../model/select-workspace-page' import { SelectWorkspacePage } from '../model/select-workspace-page'
import { ChannelPage } from '../model/channel-page'
test.use({ test.use({
storageState: PlatformSetting storageState: PlatformSetting
@ -171,6 +180,9 @@ test.describe('Planning ToDo tests', () => {
test('Adding ToDo by dragging and checking visibility in the Team Planner', async ({ browser, page, request }) => { test('Adding ToDo by dragging and checking visibility in the Team Planner', async ({ browser, page, request }) => {
const data: TestData = generateTestData() const data: TestData = generateTestData()
const leftMenuPage = new LeftSideMenuPage(page)
const channelPage = new ChannelPage(page)
const newUser2: SignUpData = { const newUser2: SignUpData = {
firstName: faker.person.firstName(), firstName: faker.person.firstName(),
lastName: faker.person.lastName(), lastName: faker.person.lastName(),
@ -213,16 +225,11 @@ test.describe('Planning ToDo tests', () => {
await planningPage.buttonPopupOnlyVisibleToYou().click() await planningPage.buttonPopupOnlyVisibleToYou().click()
await planningPage.buttonPopupSave().click() await planningPage.buttonPopupSave().click()
await leftSideMenuPage.openProfileMenu() const linkText = await getInviteLink(page)
await leftSideMenuPage.inviteToWorkspace()
await leftSideMenuPage.getInviteLink()
const linkText = await page.locator('.antiPopup .link').textContent()
const page2 = await browser.newPage()
const leftSideMenuPageSecond = new LeftSideMenuPage(page2)
await api.createAccount(newUser2.email, newUser2.password, newUser2.firstName, newUser2.lastName) await api.createAccount(newUser2.email, newUser2.password, newUser2.firstName, newUser2.lastName)
await page2.goto(linkText ?? '') using _page2 = await getSecondPageByInvite(browser, linkText, newUser2)
const joinPage = new SignInJoinPage(page2) const page2 = _page2.page
await joinPage.join(newUser2) const leftSideMenuPageSecond = new LeftSideMenuPage(page2)
await leftSideMenuPageSecond.clickTeam() await leftSideMenuPageSecond.clickTeam()
const teamPage = new TeamPage(page2) const teamPage = new TeamPage(page2)
@ -234,6 +241,40 @@ test.describe('Planning ToDo tests', () => {
.locator('div.item', { hasText: 'Busy 30m' }) .locator('div.item', { hasText: 'Busy 30m' })
.isVisible() .isVisible()
await page2.close() await test.step('Go to another page to check work in Sidebar', async () => {
await leftMenuPage.clickChunter()
await channelPage.clickChannel('general')
})
const sidebarPage = new SidebarPage(page)
await test.step('Check visibility of task in sidebar planner', async () => {
await sidebarPage.clickSidebarPageButton('calendar')
await sidebarPage.checkIfPlanerSidebarTabIsOpen(true)
})
await test.step('Change event title from sidebar calendar', async () => {
await sidebarPage.plannerSidebarNextDayButton().click()
await planningPage.eventInSidebarSchedule(titleV).click()
await planningPage.buttonPopupCreateVisible().click()
await planningPage.buttonPopupOnlyVisibleToYou().click()
await planningPage.buttonPopupSave().click()
})
})
test('User is able to open Planner in Sidebar', async ({ browser, page, request }) => {
const leftMenuPage = new LeftSideMenuPage(page)
const channelPage = new ChannelPage(page)
await test.step('Go to any another page', async () => {
await leftMenuPage.clickChunter()
await channelPage.clickChannel('general')
})
await test.step('Open planner via sidebar icon button', async () => {
const sidebarPage = new SidebarPage(page)
await sidebarPage.clickSidebarPageButton('calendar')
await sidebarPage.checkIfPlanerSidebarTabIsOpen(true)
})
}) })
}) })

View File

@ -74,11 +74,15 @@ test.describe('candidate/talents tests', () => {
}) })
test('Merge contacts', async () => { test('Merge contacts', async () => {
const firstLocation = 'Location 1'
const secondLocation = 'Location 2'
await navigationMenuPage.clickButtonTalents() await navigationMenuPage.clickButtonTalents()
// talent1 // talent1
const talentNameFirst = await talentsPage.createNewTalent() const talentNameFirst = await talentsPage.createNewTalent()
await talentsPage.openTalentByTalentName(talentNameFirst) await talentsPage.openTalentByTalentName(talentNameFirst)
await talentDetailsPage.inputLocation().fill('Awesome Location Merge1')
await talentDetailsPage.enterLocation(firstLocation)
const titleTalent1 = 'TitleMerge1' const titleTalent1 = 'TitleMerge1'
await talentDetailsPage.addTitle(titleTalent1) await talentDetailsPage.addTitle(titleTalent1)
const sourceTalent1 = 'SourceTalent1' const sourceTalent1 = 'SourceTalent1'
@ -89,7 +93,8 @@ test.describe('candidate/talents tests', () => {
await navigationMenuPage.clickButtonTalents() await navigationMenuPage.clickButtonTalents()
const talentNameSecond = await talentsPage.createNewTalent() const talentNameSecond = await talentsPage.createNewTalent()
await talentsPage.openTalentByTalentName(talentNameSecond) await talentsPage.openTalentByTalentName(talentNameSecond)
await talentDetailsPage.inputLocation().fill('Awesome Location Merge2')
await talentDetailsPage.enterLocation(secondLocation)
const titleTalent2 = 'TitleMerge2' const titleTalent2 = 'TitleMerge2'
await talentDetailsPage.addTitle(titleTalent2) await talentDetailsPage.addTitle(titleTalent2)
const sourceTalent2 = 'SourceTalent2' const sourceTalent2 = 'SourceTalent2'
@ -104,7 +109,7 @@ test.describe('candidate/talents tests', () => {
finalContactName: talentNameSecond.lastName, finalContactName: talentNameSecond.lastName,
name: `${talentNameFirst.lastName} ${talentNameFirst.firstName}`, name: `${talentNameFirst.lastName} ${talentNameFirst.firstName}`,
mergeLocation: true, mergeLocation: true,
location: 'Awesome Location Merge1', location: firstLocation,
mergeTitle: true, mergeTitle: true,
title: titleTalent1, title: titleTalent1,
mergeSource: true, mergeSource: true,
@ -116,7 +121,7 @@ test.describe('candidate/talents tests', () => {
await talentsPage.openTalentByTalentName(talentNameFirst) await talentsPage.openTalentByTalentName(talentNameFirst)
await talentDetailsPage.checkSocialLinks('Phone', '123123213213') await talentDetailsPage.checkSocialLinks('Phone', '123123213213')
await talentDetailsPage.checkSocialLinks('Email', 'test-merge-2@gmail.com') await talentDetailsPage.checkSocialLinks('Email', 'test-merge-2@gmail.com')
await talentDetailsPage.checkMergeContacts('Awesome Location Merge1', titleTalent2, sourceTalent2) await talentDetailsPage.checkMergeContacts(firstLocation, titleTalent2, sourceTalent2)
}) })
test('Match to vacancy', async () => { test('Match to vacancy', async () => {