From 9d0612a1b8ac1efe07730496380db1ddf2a6733f Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Tue, 23 Jul 2024 10:47:36 -0300 Subject: [PATCH 1/4] fix(Marketplace): Subscription modal wrong state (#32730) --- .changeset/chilled-yaks-beg.md | 5 ++++ .../tabs/AppStatus/AppStatus.tsx | 4 ++-- .../hooks/useAppInstallationHandler.tsx | 23 ++++++++++++++++--- .../views/marketplace/hooks/useAppMenu.tsx | 5 +--- 4 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 .changeset/chilled-yaks-beg.md diff --git a/.changeset/chilled-yaks-beg.md b/.changeset/chilled-yaks-beg.md new file mode 100644 index 000000000000..670fa24887b7 --- /dev/null +++ b/.changeset/chilled-yaks-beg.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed issue in Marketplace that caused a subscription app to show incorrect modals when subscribing diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppStatus/AppStatus.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppStatus/AppStatus.tsx index 26adddffae79..db46d87c18d8 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppStatus/AppStatus.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppStatus/AppStatus.tsx @@ -76,13 +76,13 @@ const AppStatus = ({ app, showStatus = true, isAppDetailsPage, installed, ...pro isAppPurchased, onDismiss: cancelAction, onSuccess: confirmAction, + setIsPurchased: setPurchased, }); const handleAcquireApp = useCallback(() => { setLoading(true); - setPurchased(true); appInstallationHandler(); - }, [appInstallationHandler, setLoading, setPurchased]); + }, [appInstallationHandler, setLoading]); // @TODO we should refactor this to not use the label to determine the variant const getStatusVariant = (status: appStatusSpanResponseProps) => { diff --git a/apps/meteor/client/views/marketplace/hooks/useAppInstallationHandler.tsx b/apps/meteor/client/views/marketplace/hooks/useAppInstallationHandler.tsx index 3c0c41798172..ab5962150f04 100644 --- a/apps/meteor/client/views/marketplace/hooks/useAppInstallationHandler.tsx +++ b/apps/meteor/client/views/marketplace/hooks/useAppInstallationHandler.tsx @@ -19,9 +19,17 @@ export type AppInstallationHandlerParams = { isAppPurchased?: boolean; onDismiss: () => void; onSuccess: (action: Actions | '', appPermissions?: App['permissions']) => void; + setIsPurchased: (purchased: boolean) => void; }; -export function useAppInstallationHandler({ app, action, isAppPurchased, onDismiss, onSuccess }: AppInstallationHandlerParams) { +export function useAppInstallationHandler({ + app, + action, + isAppPurchased, + onDismiss, + onSuccess, + setIsPurchased, +}: AppInstallationHandlerParams) { const dispatchToastMessage = useToastMessageDispatch(); const setModal = useSetModal(); @@ -62,7 +70,16 @@ export function useAppInstallationHandler({ app, action, isAppPurchased, onDismi if (action === 'purchase' && !isAppPurchased) { try { const data = await appsOrchestrator.buildExternalUrl(app.id, app.purchaseType, false); - setModal(); + setModal( + { + setIsPurchased(true); + openPermissionModal(); + }} + />, + ); } catch (error) { handleAPIError(error); } @@ -70,7 +87,7 @@ export function useAppInstallationHandler({ app, action, isAppPurchased, onDismi } openPermissionModal(); - }, [action, isAppPurchased, openPermissionModal, appsOrchestrator, app.id, app.purchaseType, setModal, onDismiss]); + }, [action, isAppPurchased, openPermissionModal, appsOrchestrator, app.id, app.purchaseType, setModal, onDismiss, setIsPurchased]); return useCallback(async () => { if (app?.versionIncompatible) { diff --git a/apps/meteor/client/views/marketplace/hooks/useAppMenu.tsx b/apps/meteor/client/views/marketplace/hooks/useAppMenu.tsx index 25ae9bc0ead8..bd2071fe2d82 100644 --- a/apps/meteor/client/views/marketplace/hooks/useAppMenu.tsx +++ b/apps/meteor/client/views/marketplace/hooks/useAppMenu.tsx @@ -91,10 +91,6 @@ export const useAppMenu = (app: App, isAppDetailsPage: boolean) => { const installationSuccess = useCallback( async (action: Actions | '', permissionsGranted) => { if (action) { - if (action === 'purchase') { - setPurchased(true); - } - if (action === 'request') { setRequestedEndUser(true); } else { @@ -119,6 +115,7 @@ export const useAppMenu = (app: App, isAppDetailsPage: boolean) => { action, onDismiss: closeModal, onSuccess: installationSuccess, + setIsPurchased: setPurchased, }); const handleAcquireApp = useCallback(() => { From be1f1a21b3b1d1244d3b22f331a0c191480e8538 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Tue, 23 Jul 2024 12:51:30 -0300 Subject: [PATCH 2/4] test: flaky tests related to waitForChannel (#32862) --- apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts index c0cb13a3b5b4..f8002ed450f5 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts @@ -114,8 +114,8 @@ export class HomeSidenav { async waitForChannel(): Promise { await this.page.locator('role=main').waitFor(); await this.page.locator('role=main >> role=heading[level=1]').waitFor(); + await this.page.locator('role=main >> role=list').waitFor(); - await expect(this.page.locator('role=main >> .rcx-skeleton')).toHaveCount(0); await expect(this.page.locator('role=main >> role=list')).not.toHaveAttribute('aria-busy', 'true'); } From 264d7d5496532d8869a878fc30250517465fcdca Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Tue, 23 Jul 2024 13:57:28 -0300 Subject: [PATCH 3/4] feat(Omnichannel): Queued Status option Current Chats (#32800) Co-authored-by: Kevin Aleman <11577696+KevLehman@users.noreply.github.com> --- .changeset/weak-tigers-suffer.md | 7 ++ .../app/livechat/imports/server/rest/rooms.ts | 3 +- .../app/livechat/server/api/lib/rooms.ts | 3 + .../currentChats/CurrentChatsPage.tsx | 9 ++- .../omnichannel/currentChats/FilterByText.tsx | 1 + .../meteor/server/models/raw/LivechatRooms.ts | 12 ++++ .../tests/end-to-end/api/livechat/00-rooms.ts | 72 +++++++++++++++++++ .../src/models/ILivechatRoomsModel.ts | 2 + packages/rest-typings/src/v1/omnichannel.ts | 7 ++ 9 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 .changeset/weak-tigers-suffer.md diff --git a/.changeset/weak-tigers-suffer.md b/.changeset/weak-tigers-suffer.md new file mode 100644 index 000000000000..91748a43c677 --- /dev/null +++ b/.changeset/weak-tigers-suffer.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/model-typings": minor +"@rocket.chat/rest-typings": minor +--- + +Added the ability to filter chats by `queued` on the Current Chats Omnichannel page diff --git a/apps/meteor/app/livechat/imports/server/rest/rooms.ts b/apps/meteor/app/livechat/imports/server/rest/rooms.ts index f7d5ddb314c9..f80ed61a131e 100644 --- a/apps/meteor/app/livechat/imports/server/rest/rooms.ts +++ b/apps/meteor/app/livechat/imports/server/rest/rooms.ts @@ -30,7 +30,7 @@ API.v1.addRoute( async get() { const { offset, count } = await getPaginationItems(this.queryParams); const { sort, fields } = await this.parseJsonQuery(); - const { agents, departmentId, open, tags, roomName, onhold } = this.queryParams; + const { agents, departmentId, open, tags, roomName, onhold, queued } = this.queryParams; const { createdAt, customFields, closedAt } = this.queryParams; const createdAtParam = validateDateParams('createdAt', createdAt); @@ -69,6 +69,7 @@ API.v1.addRoute( tags, customFields: parsedCf, onhold, + queued, options: { offset, count, sort, fields }, }), ); diff --git a/apps/meteor/app/livechat/server/api/lib/rooms.ts b/apps/meteor/app/livechat/server/api/lib/rooms.ts index b130e5c2c73a..26449dce3963 100644 --- a/apps/meteor/app/livechat/server/api/lib/rooms.ts +++ b/apps/meteor/app/livechat/server/api/lib/rooms.ts @@ -14,6 +14,7 @@ export async function findRooms({ tags, customFields, onhold, + queued, options: { offset, count, fields, sort }, }: { agents?: Array; @@ -31,6 +32,7 @@ export async function findRooms({ tags?: Array; customFields?: Record; onhold?: string | boolean; + queued?: string | boolean; options: { offset: number; count: number; fields: Record; sort: Record }; }): Promise }>> { const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); @@ -44,6 +46,7 @@ export async function findRooms({ tags, customFields, onhold: ['t', 'true', '1'].includes(`${onhold}`), + queued: ['t', 'true', '1'].includes(`${queued}`), options: { sort: sort || { ts: -1 }, offset, diff --git a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx index 95fb9a54c3ce..c439cc838874 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx @@ -54,6 +54,7 @@ type CurrentChatQuery = { customFields?: string; sort: string; count?: number; + queued?: boolean; }; type useQueryType = ( @@ -95,8 +96,9 @@ const currentChatQuery: useQueryType = ( } if (status !== 'all') { - query.open = status === 'opened' || status === 'onhold'; + query.open = status === 'opened' || status === 'onhold' || status === 'queued'; query.onhold = status === 'onhold'; + query.queued = status === 'queued'; } if (servedBy && servedBy !== 'all') { query.agents = [servedBy]; @@ -170,8 +172,9 @@ const CurrentChatsPage = ({ id, onRowClick }: { id?: string; onRowClick: (_id: s const renderRow = useCallback( (room) => { const { _id, fname, servedBy, ts, lm, department, open, onHold, priorityWeight } = room; - const getStatusText = (open: boolean, onHold: boolean): string => { + const getStatusText = (open: boolean, onHold: boolean, servedBy: boolean): string => { if (!open) return t('Closed'); + if (open && !servedBy) return t('Queued'); return onHold ? t('On_Hold_Chats') : t('Room_Status_Open'); }; @@ -198,7 +201,7 @@ const CurrentChatsPage = ({ id, onRowClick }: { id?: string; onRowClick: (_id: s {moment(lm).format('L LTS')} - {getStatusText(open, onHold)} + {getStatusText(open, onHold, !!servedBy?.username)} {canRemoveClosedChats && !open && } diff --git a/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx b/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx index 131aa06f0a70..cda579387ea1 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx @@ -30,6 +30,7 @@ const FilterByText = ({ setFilter, reload, customFields, setCustomFields, hasCus ['closed', t('Closed')], ['opened', t('Room_Status_Open')], ['onhold', t('On_Hold_Chats')], + ['queued', t('Queued')], ]; const [guest, setGuest] = useLocalStorage('guest', ''); diff --git a/apps/meteor/server/models/raw/LivechatRooms.ts b/apps/meteor/server/models/raw/LivechatRooms.ts index d1b704e024b9..648af95ed180 100644 --- a/apps/meteor/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/server/models/raw/LivechatRooms.ts @@ -1211,6 +1211,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive visitorId, roomIds, onhold, + queued, options = {}, extraQuery = {}, }: { @@ -1226,6 +1227,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive visitorId?: string; roomIds?: string[]; onhold?: boolean; + queued?: boolean; options?: { offset?: number; count?: number; sort?: { [k: string]: SortDirection } }; extraQuery?: Filter; }) { @@ -1242,6 +1244,10 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive ...(visitorId && visitorId !== 'undefined' && { 'v._id': visitorId }), }; + if (open) { + query.servedBy = { $exists: true }; + } + if (createdAt) { query.ts = {}; if (createdAt.start) { @@ -1280,6 +1286,12 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive }; } + if (queued) { + query.servedBy = { $exists: false }; + query.open = true; + query.onHold = { $ne: true }; + } + return this.findPaginated(query, { sort: options.sort || { name: 1 }, skip: options.offset, diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index 5c881b530d08..23f6d35d2acd 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -35,6 +35,7 @@ import { fetchMessages, deleteVisitor, makeAgentUnavailable, + sendAgentMessage, } from '../../../data/livechat/rooms'; import { saveTags } from '../../../data/livechat/tags'; import type { DummyResponse } from '../../../data/livechat/utils'; @@ -341,6 +342,77 @@ describe('LIVECHAT - rooms', () => { expect(body.rooms.some((room: IOmnichannelRoom) => !!room.closedAt)).to.be.true; expect(body.rooms.some((room: IOmnichannelRoom) => room.open)).to.be.true; }); + it('should return queued rooms when `queued` param is passed', async () => { + await updateSetting('Livechat_Routing_Method', 'Manual_Selection'); + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + + const { body } = await request.get(api('livechat/rooms')).query({ queued: true }).set(credentials).expect(200); + + expect(body.rooms.every((room: IOmnichannelRoom) => room.open)).to.be.true; + expect(body.rooms.every((room: IOmnichannelRoom) => !room.servedBy)).to.be.true; + expect(body.rooms.find((froom: IOmnichannelRoom) => froom._id === room._id)).to.be.not.undefined; + }); + it('should return queued rooms when `queued` and `open` params are passed', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + + const { body } = await request.get(api('livechat/rooms')).query({ queued: true, open: true }).set(credentials).expect(200); + + expect(body.rooms.every((room: IOmnichannelRoom) => room.open)).to.be.true; + expect(body.rooms.every((room: IOmnichannelRoom) => !room.servedBy)).to.be.true; + expect(body.rooms.find((froom: IOmnichannelRoom) => froom._id === room._id)).to.be.not.undefined; + }); + it('should return open rooms when `open` is param is passed. Open rooms should not include queued conversations', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + + const { room: room2 } = await startANewLivechatRoomAndTakeIt(); + + const { body } = await request.get(api('livechat/rooms')).query({ open: true }).set(credentials).expect(200); + + expect(body.rooms.every((room: IOmnichannelRoom) => room.open)).to.be.true; + expect(body.rooms.find((froom: IOmnichannelRoom) => froom._id === room2._id)).to.be.not.undefined; + expect(body.rooms.find((froom: IOmnichannelRoom) => froom._id === room._id)).to.be.undefined; + + await updateSetting('Livechat_Routing_Method', 'Auto_Selection'); + }); + (IS_EE ? describe : describe.skip)('Queued and OnHold chats', () => { + before(async () => { + await updateSetting('Livechat_allow_manual_on_hold', true); + await updateSetting('Livechat_Routing_Method', 'Manual_Selection'); + }); + + after(async () => { + await updateSetting('Livechat_Routing_Method', 'Auto_Selection'); + await updateSetting('Livechat_allow_manual_on_hold', false); + }); + + it('should not return on hold rooms along with queued rooms when `queued` is true and `onHold` is true', async () => { + const { room } = await startANewLivechatRoomAndTakeIt(); + await sendAgentMessage(room._id); + const response = await request + .post(api('livechat/room.onHold')) + .set(credentials) + .send({ + roomId: room._id, + }) + .expect(200); + + expect(response.body.success).to.be.true; + + const visitor = await createVisitor(); + const room2 = await createLivechatRoom(visitor.token); + + const { body } = await request.get(api('livechat/rooms')).query({ queued: true, onhold: true }).set(credentials).expect(200); + + expect(body.rooms.every((room: IOmnichannelRoom) => room.open)).to.be.true; + expect(body.rooms.every((room: IOmnichannelRoom) => !room.servedBy)).to.be.true; + expect(body.rooms.every((room: IOmnichannelRoom) => !room.onHold)).to.be.true; + expect(body.rooms.find((froom: IOmnichannelRoom) => froom._id === room._id)).to.be.undefined; + expect(body.rooms.find((froom: IOmnichannelRoom) => froom._id === room2._id)).to.be.not.undefined; + }); + }); (IS_EE ? it : it.skip)('should return only rooms with the given department', async () => { const { department } = await createDepartmentWithAnOnlineAgent(); diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index 6384b325f991..d4da1d7d8159 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -28,6 +28,7 @@ type WithOptions = { options?: any; }; +// TODO: Fix types of model export interface ILivechatRoomsModel extends IBaseModel { getQueueMetrics(params: { departmentId: any; agentId: any; includeOfflineAgents: any; options?: any }): any; @@ -96,6 +97,7 @@ export interface ILivechatRoomsModel extends IBaseModel { visitorId?: any; roomIds?: any; onhold: any; + queued: any; options?: any; extraQuery?: any; }): FindPaginated>; diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 3dad53cbe8d2..ec53304605fc 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -2552,6 +2552,7 @@ export type GETLivechatRoomsParams = PaginatedRequest<{ departmentId?: string; open?: string | boolean; onhold?: string | boolean; + queued?: string | boolean; tags?: string[]; }>; @@ -2617,6 +2618,12 @@ const GETLivechatRoomsParamsSchema = { { type: 'boolean', nullable: true }, ], }, + queued: { + anyOf: [ + { type: 'string', nullable: true }, + { type: 'boolean', nullable: true }, + ], + }, tags: { type: 'array', items: { From 7e8e003772724573fb1019f8146250eccf2eeb6a Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 23 Jul 2024 14:07:08 -0300 Subject: [PATCH 4/4] test: e2e-encryption use fill and restore api session (#32866) --- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 19 +++++++------------ .../page-objects/fragments/home-content.ts | 4 ++-- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 55aefe88571c..6f73fb6b5961 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -594,6 +594,10 @@ test.describe.serial('e2ee room setup', () => { expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); }); + test.afterEach(async ({ api }) => { + await api.recreateContext(); + }); + test('expect save password state on encrypted room', async ({ page }) => { await page.goto('/account/security'); await poAccountProfile.securityE2EEncryptionSection.click(); @@ -601,16 +605,11 @@ test.describe.serial('e2ee room setup', () => { await page.locator('role=button[name="Login"]').waitFor(); - await page.reload(); - - await page.locator('role=button[name="Login"]').waitFor(); - await injectInitialData(); await restoreState(page, Users.admin); await page.goto('/home'); - await page.locator('role=banner >> text="Save your encryption password"').waitFor(); await expect(page.locator('role=banner >> text="Save your encryption password"')).toBeVisible(); const channelName = faker.string.uuid(); @@ -624,10 +623,8 @@ test.describe.serial('e2ee room setup', () => { await poHomeChannel.dismissToast(); - await poHomeChannel.content.encryptedRoomHeaderIcon.first().waitFor(); await expect(poHomeChannel.content.encryptedRoomHeaderIcon.first()).toBeVisible(); - await page.locator('role=button[name="Save E2EE password"]').waitFor(); await expect(page.locator('role=button[name="Save E2EE password"]')).toBeVisible(); await poHomeChannel.tabs.btnE2EERoomSetupDisableE2E.waitFor(); @@ -660,8 +657,6 @@ test.describe.serial('e2ee room setup', () => { // Logout to remove e2ee keys await poHomeChannel.sidenav.logout(); - await page.locator('role=button[name="Login"]').waitFor(); - await page.reload(); await page.locator('role=button[name="Login"]').waitFor(); await injectInitialData(); @@ -781,11 +776,11 @@ test.describe.serial('e2ee support legacy formats', () => { }); test.afterAll(async ({ api }) => { - expect((await api.post('/settings/E2E_Enable', { value: false })).status()).toBe(200); - expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); + await api.post('/settings/E2E_Enable', { value: false }); + await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false }); }); - // Not testing upload since it was not implemented in the legacy format + // ->>>>>>>>>>>Not testing upload since it was not implemented in the legacy format test('expect create a private channel encrypted and send an encrypted message', async ({ page, request }) => { await page.goto('/home'); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 8310378e55b6..ec761ddbfa73 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -82,7 +82,7 @@ export class HomeContent { async sendMessage(text: string): Promise { await this.joinRoomIfNeeded(); await this.page.waitForSelector('[name="msg"]:not([disabled])'); - await this.page.locator('[name="msg"]').type(text); + await this.page.locator('[name="msg"]').fill(text); await this.page.keyboard.press('Enter'); } @@ -90,7 +90,7 @@ export class HomeContent { await this.joinRoomIfNeeded(); await this.page.waitForSelector('[name="msg"]:not([disabled])'); await this.page.locator('[name="msg"]').fill(''); - await this.page.locator('[name="msg"]').type(text); + await this.page.locator('[name="msg"]').fill(text); await this.page.keyboard.press('Enter'); await this.page.keyboard.press('Enter'); }