fix: fix 10-settings-log-streaming e2e tests

This commit is contained in:
Alex Grozav 2023-07-24 18:29:09 +03:00
parent 4b058cdcc2
commit 810567c4e8
8 changed files with 80 additions and 95 deletions

View file

@ -1,4 +1,6 @@
import { SettingsLogStreamingPage } from '../pages'; import { SettingsLogStreamingPage } from '../pages';
import { getVisibleModalOverlay } from '../utils/modal';
import { getVisibleDropdown } from '../utils';
const settingsLogStreamingPage = new SettingsLogStreamingPage(); const settingsLogStreamingPage = new SettingsLogStreamingPage();
@ -19,6 +21,7 @@ describe('Log Streaming Settings', () => {
}); });
it('should show the add destination modal', () => { it('should show the add destination modal', () => {
cy.enableFeature('logStreaming');
cy.visit('/settings/log-streaming'); cy.visit('/settings/log-streaming');
settingsLogStreamingPage.actions.clickAddFirstDestination(); settingsLogStreamingPage.actions.clickAddFirstDestination();
cy.wait(100); cy.wait(100);
@ -27,7 +30,7 @@ describe('Log Streaming Settings', () => {
settingsLogStreamingPage.getters.getSelectDestinationButton().should('be.visible'); settingsLogStreamingPage.getters.getSelectDestinationButton().should('be.visible');
settingsLogStreamingPage.getters.getSelectDestinationButton().should('have.attr', 'disabled'); settingsLogStreamingPage.getters.getSelectDestinationButton().should('have.attr', 'disabled');
settingsLogStreamingPage.getters settingsLogStreamingPage.getters
.getDestinationModalDialog() .getDestinationModal()
.invoke('css', 'width') .invoke('css', 'width')
.then((widthStr) => parseInt((widthStr as unknown as string).replace('px', ''))) .then((widthStr) => parseInt((widthStr as unknown as string).replace('px', '')))
.should('be.lessThan', 500); .should('be.lessThan', 500);
@ -36,11 +39,12 @@ describe('Log Streaming Settings', () => {
settingsLogStreamingPage.getters settingsLogStreamingPage.getters
.getSelectDestinationButton() .getSelectDestinationButton()
.should('not.have.attr', 'disabled'); .should('not.have.attr', 'disabled');
settingsLogStreamingPage.getters.getDestinationModal().click(1, 1); getVisibleModalOverlay().click(1, 1);
settingsLogStreamingPage.getters.getDestinationModal().should('not.exist'); settingsLogStreamingPage.getters.getDestinationModal().should('not.exist');
}); });
it('should create a destination and delete it', () => { it('should create a destination and delete it', () => {
cy.enableFeature('logStreaming');
cy.visit('/settings/log-streaming'); cy.visit('/settings/log-streaming');
settingsLogStreamingPage.actions.clickAddFirstDestination(); settingsLogStreamingPage.actions.clickAddFirstDestination();
cy.wait(100); cy.wait(100);
@ -48,22 +52,26 @@ describe('Log Streaming Settings', () => {
settingsLogStreamingPage.getters.getSelectDestinationType().click(); settingsLogStreamingPage.getters.getSelectDestinationType().click();
settingsLogStreamingPage.getters.getSelectDestinationTypeItems().eq(0).click(); settingsLogStreamingPage.getters.getSelectDestinationTypeItems().eq(0).click();
settingsLogStreamingPage.getters.getSelectDestinationButton().click(); settingsLogStreamingPage.getters.getSelectDestinationButton().click();
settingsLogStreamingPage.getters.getDestinationNameInput().click() settingsLogStreamingPage.getters.getDestinationNameInput().click();
settingsLogStreamingPage.getters.getDestinationNameInput().find('input').clear().type('Destination 0'); settingsLogStreamingPage.getters
.getDestinationNameInput()
.find('input')
.clear()
.type('Destination 0');
settingsLogStreamingPage.getters.getDestinationSaveButton().click(); settingsLogStreamingPage.getters.getDestinationSaveButton().click();
cy.wait(100); cy.wait(100);
settingsLogStreamingPage.getters.getDestinationModal().click(1, 1); getVisibleModalOverlay().click(1, 1);
cy.reload(); cy.reload();
settingsLogStreamingPage.getters.getDestinationCards().eq(0).click(); settingsLogStreamingPage.getters.getDestinationCards().eq(0).click();
settingsLogStreamingPage.getters.getDestinationDeleteButton().should('be.visible').click(); settingsLogStreamingPage.getters.getDestinationDeleteButton().should('be.visible').click();
cy.get('.el-message-box').should('be.visible').find('.btn--cancel').click(); cy.get('.el-message-box').should('be.visible').find('.btn--cancel').click();
settingsLogStreamingPage.getters.getDestinationDeleteButton().click(); settingsLogStreamingPage.getters.getDestinationDeleteButton().click();
cy.get('.el-message-box').should('be.visible').find('.btn--confirm').click(); cy.get('.el-message-box').should('be.visible').find('.btn--confirm').click();
cy.reload();
}); });
it('should create a destination and delete it via card actions', () => { it('should create a destination and delete it via card actions', () => {
cy.enableFeature('logStreaming');
cy.visit('/settings/log-streaming'); cy.visit('/settings/log-streaming');
settingsLogStreamingPage.actions.clickAddFirstDestination(); settingsLogStreamingPage.actions.clickAddFirstDestination();
cy.wait(100); cy.wait(100);
@ -71,30 +79,25 @@ describe('Log Streaming Settings', () => {
settingsLogStreamingPage.getters.getSelectDestinationType().click(); settingsLogStreamingPage.getters.getSelectDestinationType().click();
settingsLogStreamingPage.getters.getSelectDestinationTypeItems().eq(0).click(); settingsLogStreamingPage.getters.getSelectDestinationTypeItems().eq(0).click();
settingsLogStreamingPage.getters.getSelectDestinationButton().click(); settingsLogStreamingPage.getters.getSelectDestinationButton().click();
settingsLogStreamingPage.getters.getDestinationNameInput().click() settingsLogStreamingPage.getters.getDestinationNameInput().click();
settingsLogStreamingPage.getters.getDestinationNameInput().find('input').clear().type('Destination 1'); settingsLogStreamingPage.getters
.getDestinationNameInput()
.find('input')
.clear()
.type('Destination 1');
settingsLogStreamingPage.getters.getDestinationSaveButton().should('not.have.attr', 'disabled'); settingsLogStreamingPage.getters.getDestinationSaveButton().should('not.have.attr', 'disabled');
settingsLogStreamingPage.getters.getDestinationSaveButton().click(); settingsLogStreamingPage.getters.getDestinationSaveButton().click();
cy.wait(100); cy.wait(100);
settingsLogStreamingPage.getters.getDestinationModal().click(1, 1); getVisibleModalOverlay().click(1, 1);
cy.reload(); cy.reload();
settingsLogStreamingPage.getters settingsLogStreamingPage.getters.getDestinationCards().eq(0).find('.el-dropdown').click();
.getDestinationCards() getVisibleDropdown().find('.el-dropdown-menu__item').eq(0).click();
.eq(0)
.find('.el-dropdown-selfdefine')
.click();
cy.get('.el-dropdown-menu').find('.el-dropdown-menu__item').eq(0).click();
settingsLogStreamingPage.getters.getDestinationSaveButton().should('not.exist'); settingsLogStreamingPage.getters.getDestinationSaveButton().should('not.exist');
settingsLogStreamingPage.getters.getDestinationModal().click(1, 1); getVisibleModalOverlay().click(1, 1);
settingsLogStreamingPage.getters settingsLogStreamingPage.getters.getDestinationCards().eq(0).find('.el-dropdown').click();
.getDestinationCards() getVisibleDropdown().find('.el-dropdown-menu__item').eq(1).click();
.eq(0)
.find('.el-dropdown-selfdefine')
.click();
cy.get('.el-dropdown-menu').find('.el-dropdown-menu__item').eq(1).click();
cy.get('.el-message-box').should('be.visible').find('.btn--confirm').click(); cy.get('.el-message-box').should('be.visible').find('.btn--confirm').click();
cy.reload();
}); });
}); });

View file

@ -6,9 +6,11 @@ import {
} from '../constants'; } from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows'; import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
import { getVisibleDropdown, getVisibleSelect } from '../utils';
const NEW_WORKFLOW_NAME = 'Something else'; const NEW_WORKFLOW_NAME = 'Something else';
const IMPORT_WORKFLOW_URL = 'https://gist.githubusercontent.com/OlegIvaniv/010bd3f45c8a94f8eb7012e663a8b671/raw/3afea1aec15573cc168d9af7e79395bd76082906/test-workflow.json'; const IMPORT_WORKFLOW_URL =
'https://gist.githubusercontent.com/OlegIvaniv/010bd3f45c8a94f8eb7012e663a8b671/raw/3afea1aec15573cc168d9af7e79395bd76082906/test-workflow.json';
const DUPLICATE_WORKFLOW_NAME = 'Duplicated workflow'; const DUPLICATE_WORKFLOW_NAME = 'Duplicated workflow';
const DUPLICATE_WORKFLOW_TAG = 'Duplicate'; const DUPLICATE_WORKFLOW_TAG = 'Duplicate';
@ -67,11 +69,11 @@ describe('Workflow Actions', () => {
it('should not save workflow if canvas is loading', () => { it('should not save workflow if canvas is loading', () => {
let interceptCalledCount = 0; let interceptCalledCount = 0;
// There's no way in Cypress to check if intercept was not called // There's no way in Cypress to check if intercept was not called
// so we'll count the number of times it was called // so we'll count the number of times it was called
cy.intercept('PATCH', '/rest/workflows/*', () => { cy.intercept('PATCH', '/rest/workflows/*', () => {
interceptCalledCount++; interceptCalledCount++;
}).as('saveWorkflow'); }).as('saveWorkflow');
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.saveWorkflowOnButtonClick(); WorkflowPage.actions.saveWorkflowOnButtonClick();
@ -84,11 +86,11 @@ describe('Workflow Actions', () => {
(req) => { (req) => {
// Delay the response to give time for the save to be triggered // Delay the response to give time for the save to be triggered
req.on('response', async (res) => { req.on('response', async (res) => {
await new Promise((resolve) => setTimeout(resolve, 2000)) await new Promise((resolve) => setTimeout(resolve, 2000));
res.send(); res.send();
}) });
} },
) );
cy.reload(); cy.reload();
cy.get('.el-loading-mask').should('exist'); cy.get('.el-loading-mask').should('exist');
cy.get('body').type(META_KEY, { release: false }).type('s'); cy.get('body').type(META_KEY, { release: false }).type('s');
@ -99,7 +101,7 @@ describe('Workflow Actions', () => {
cy.get('body').type(META_KEY, { release: false }).type('s'); cy.get('body').type(META_KEY, { release: false }).type('s');
cy.wait('@saveWorkflow'); cy.wait('@saveWorkflow');
cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(1)); cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(1));
}) });
it('should copy nodes', () => { it('should copy nodes', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
@ -127,7 +129,7 @@ describe('Workflow Actions', () => {
cy.get('.el-message-box').should('be.visible'); cy.get('.el-message-box').should('be.visible');
cy.get('.el-message-box').find('input').type(IMPORT_WORKFLOW_URL); cy.get('.el-message-box').find('input').type(IMPORT_WORKFLOW_URL);
cy.get('body').type('{enter}'); cy.get('body').type('{enter}');
cy.waitForLoad(false) cy.waitForLoad(false);
WorkflowPage.actions.zoomToFit(); WorkflowPage.actions.zoomToFit();
WorkflowPage.getters.canvasNodes().should('have.length', 2); WorkflowPage.getters.canvasNodes().should('have.length', 2);
WorkflowPage.getters.nodeConnections().should('have.length', 1); WorkflowPage.getters.nodeConnections().should('have.length', 1);
@ -137,7 +139,7 @@ describe('Workflow Actions', () => {
WorkflowPage.getters WorkflowPage.getters
.workflowImportInput() .workflowImportInput()
.selectFile('cypress/fixtures/Test_workflow-actions_paste-data.json', { force: true }); .selectFile('cypress/fixtures/Test_workflow-actions_paste-data.json', { force: true });
cy.waitForLoad(false) cy.waitForLoad(false);
WorkflowPage.actions.zoomToFit(); WorkflowPage.actions.zoomToFit();
WorkflowPage.getters.canvasNodes().should('have.length', 2); WorkflowPage.getters.canvasNodes().should('have.length', 2);
WorkflowPage.getters.nodeConnections().should('have.length', 1); WorkflowPage.getters.nodeConnections().should('have.length', 1);
@ -157,57 +159,33 @@ describe('Workflow Actions', () => {
WorkflowPage.getters.workflowMenuItemSettings().click(); WorkflowPage.getters.workflowMenuItemSettings().click();
// Change all settings // Change all settings
// totalWorkflows + 1 (current workflow) + 1 (no workflow option) // totalWorkflows + 1 (current workflow) + 1 (no workflow option)
WorkflowPage.getters.workflowSettingsErrorWorkflowSelect().find('li').should('have.length', totalWorkflows + 2); WorkflowPage.getters.workflowSettingsErrorWorkflowSelect().click();
WorkflowPage.getters getVisibleSelect()
.workflowSettingsErrorWorkflowSelect()
.find('li') .find('li')
.last() .should('have.length', totalWorkflows + 2);
.click({ force: true }); getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters.workflowSettingsTimezoneSelect().find('li').should('exist'); WorkflowPage.getters.workflowSettingsTimezoneSelect().click();
WorkflowPage.getters.workflowSettingsTimezoneSelect().find('li').eq(1).click({ force: true }); getVisibleSelect().find('li').should('exist');
WorkflowPage.getters getVisibleSelect().find('li').eq(1).click({ force: true });
.workflowSettingsSaveFiledExecutionsSelect() WorkflowPage.getters.workflowSettingsSaveFiledExecutionsSelect().click();
.find('li') getVisibleSelect().find('li').should('have.length', 3);
.should('have.length', 3); getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters WorkflowPage.getters.workflowSettingsSaveSuccessExecutionsSelect().click();
.workflowSettingsSaveFiledExecutionsSelect() getVisibleSelect().find('li').should('have.length', 3);
.find('li') getVisibleSelect().find('li').last().click({ force: true });
.last() WorkflowPage.getters.workflowSettingsSaveManualExecutionsSelect().click();
.click({ force: true }); getVisibleSelect().find('li').should('have.length', 3);
WorkflowPage.getters getVisibleSelect().find('li').last().click({ force: true });
.workflowSettingsSaveSuccessExecutionsSelect() WorkflowPage.getters.workflowSettingsSaveExecutionProgressSelect().click();
.find('li') getVisibleSelect().find('li').should('have.length', 3);
.should('have.length', 3); getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters
.workflowSettingsSaveSuccessExecutionsSelect()
.find('li')
.last()
.click({ force: true });
WorkflowPage.getters
.workflowSettingsSaveManualExecutionsSelect()
.find('li')
.should('have.length', 3);
WorkflowPage.getters
.workflowSettingsSaveManualExecutionsSelect()
.find('li')
.last()
.click({ force: true });
WorkflowPage.getters
.workflowSettingsSaveExecutionProgressSelect()
.find('li')
.should('have.length', 3);
WorkflowPage.getters
.workflowSettingsSaveExecutionProgressSelect()
.find('li')
.last()
.click({ force: true });
WorkflowPage.getters.workflowSettingsTimeoutWorkflowSwitch().click(); WorkflowPage.getters.workflowSettingsTimeoutWorkflowSwitch().click();
WorkflowPage.getters.workflowSettingsTimeoutForm().find('input').first().type('1'); WorkflowPage.getters.workflowSettingsTimeoutForm().find('input').first().type('1');
// Save settings // Save settings
WorkflowPage.getters.workflowSettingsSaveButton().click(); WorkflowPage.getters.workflowSettingsSaveButton().click();
WorkflowPage.getters.workflowSettingsModal().should('not.exist'); WorkflowPage.getters.workflowSettingsModal().should('not.exist');
WorkflowPage.getters.successToast().should('exist'); WorkflowPage.getters.successToast().should('exist');
}) });
}); });
it('should not be able to delete unsaved workflow', () => { it('should not be able to delete unsaved workflow', () => {

View file

@ -1,4 +1,5 @@
import { BasePage } from './base'; import { BasePage } from './base';
import { getVisibleSelect } from '../utils';
export class SettingsLogStreamingPage extends BasePage { export class SettingsLogStreamingPage extends BasePage {
url = '/settings/log-streaming'; url = '/settings/log-streaming';
@ -6,11 +7,9 @@ export class SettingsLogStreamingPage extends BasePage {
getActionBoxUnlicensed: () => cy.getByTestId('action-box-unlicensed'), getActionBoxUnlicensed: () => cy.getByTestId('action-box-unlicensed'),
getActionBoxLicensed: () => cy.getByTestId('action-box-licensed'), getActionBoxLicensed: () => cy.getByTestId('action-box-licensed'),
getDestinationModal: () => cy.getByTestId('destination-modal'), getDestinationModal: () => cy.getByTestId('destination-modal'),
getDestinationModalDialog: () => this.getters.getDestinationModal().find('.el-dialog'),
getSelectDestinationType: () => cy.getByTestId('select-destination-type'), getSelectDestinationType: () => cy.getByTestId('select-destination-type'),
getDestinationNameInput: () => cy.getByTestId('subtitle-showing-type'), getDestinationNameInput: () => cy.getByTestId('subtitle-showing-type'),
getSelectDestinationTypeItems: () => getSelectDestinationTypeItems: () => getVisibleSelect().find('.el-select-dropdown__item'),
this.getters.getSelectDestinationType().find('.el-select-dropdown__item'),
getSelectDestinationButton: () => cy.getByTestId('select-destination-button'), getSelectDestinationButton: () => cy.getByTestId('select-destination-button'),
getContactUsButton: () => this.getters.getActionBoxUnlicensed().find('button'), getContactUsButton: () => this.getters.getActionBoxUnlicensed().find('button'),
getAddFirstDestinationButton: () => this.getters.getActionBoxLicensed().find('button'), getAddFirstDestinationButton: () => this.getters.getActionBoxLicensed().find('button'),

3
cypress/utils/modal.ts Normal file
View file

@ -0,0 +1,3 @@
export function getVisibleModalOverlay() {
return cy.get('.el-overlay .el-overlay-dialog').filter(':visible');
}

View file

@ -7,5 +7,5 @@ export function getVisibleSelect() {
} }
export function getVisibleDropdown() { export function getVisibleDropdown() {
return getVisiblePopper().filter('.el-select__dropdown'); return getVisiblePopper().filter('.el-dropdown__popper');
} }

View file

@ -1,10 +1,9 @@
<template> <template>
<span :class="$style.container" data-test-id="action-toggle"> <span @click.stop.prevent :class="$style.container" data-test-id="action-toggle">
<el-dropdown <el-dropdown
:placement="placement" :placement="placement"
:size="size" :size="size"
trigger="click" trigger="click"
@click.stop
@command="onCommand" @command="onCommand"
@visible-change="onVisibleChange" @visible-change="onVisibleChange"
> >

View file

@ -189,7 +189,7 @@ import {
defaultMessageEventBusDestinationSentryOptions, defaultMessageEventBusDestinationSentryOptions,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { defineComponent } from 'vue'; import { defineComponent, nextTick } from 'vue';
import { LOG_STREAM_MODAL_KEY, MODAL_CONFIRM } from '@/constants'; import { LOG_STREAM_MODAL_KEY, MODAL_CONFIRM } from '@/constants';
import Modal from '@/components/Modal.vue'; import Modal from '@/components/Modal.vue';
import { useMessage } from '@/composables'; import { useMessage } from '@/composables';
@ -375,9 +375,12 @@ export default defineComponent({
); );
break; break;
} }
if (newDestination) { if (newDestination) {
this.headerLabel = newDestination?.label ?? this.headerLabel; this.headerLabel = newDestination?.label ?? this.headerLabel;
this.setupNode(newDestination); nextTick(() => {
this.setupNode(newDestination);
});
} }
}, },
valueChanged(parameterData: IUpdateInformation) { valueChanged(parameterData: IUpdateInformation) {

View file

@ -40,14 +40,14 @@ export const useLogStreamingStore = defineStore('logStreaming', {
getters: {}, getters: {},
actions: { actions: {
addDestination(destination: MessageEventBusDestinationOptions) { addDestination(destination: MessageEventBusDestinationOptions) {
if (destination.id && destination.id in this.items) { if (destination.id && this.items[destination.id]) {
this.items[destination.id].destination = destination; this.items[destination.id].destination = destination;
} else { } else {
this.setSelectionAndBuildItems(destination); this.setSelectionAndBuildItems(destination);
} }
}, },
getDestination(destinationId: string): MessageEventBusDestinationOptions | undefined { getDestination(destinationId: string): MessageEventBusDestinationOptions | undefined {
if (destinationId in this.items) { if (this.items[destinationId]) {
return this.items[destinationId].destination; return this.items[destinationId].destination;
} else { } else {
return; return;
@ -61,9 +61,9 @@ export const useLogStreamingStore = defineStore('logStreaming', {
return destinations; return destinations;
}, },
updateDestination(destination: MessageEventBusDestinationOptions) { updateDestination(destination: MessageEventBusDestinationOptions) {
if (destination.id && destination.id in this.items) { if (destination.id && this.items[destination.id]) {
this.$patch((state) => { this.$patch((state) => {
if (destination.id && destination.id in this.items) { if (destination.id && this.items[destination.id]) {
state.items[destination.id].destination = destination; state.items[destination.id].destination = destination;
} }
// to trigger refresh // to trigger refresh
@ -74,7 +74,7 @@ export const useLogStreamingStore = defineStore('logStreaming', {
removeDestination(destinationId: string) { removeDestination(destinationId: string) {
if (!destinationId) return; if (!destinationId) return;
delete this.items[destinationId]; delete this.items[destinationId];
if (destinationId in this.items) { if (this.items[destinationId]) {
this.$patch({ this.$patch({
items: { items: {
...this.items, ...this.items,
@ -104,7 +104,7 @@ export const useLogStreamingStore = defineStore('logStreaming', {
}, },
getSelectedEvents(destinationId: string): string[] { getSelectedEvents(destinationId: string): string[] {
const selectedEvents: string[] = []; const selectedEvents: string[] = [];
if (destinationId in this.items) { if (this.items[destinationId]) {
for (const group of this.items[destinationId].eventGroups) { for (const group of this.items[destinationId].eventGroups) {
if (group.selected) { if (group.selected) {
selectedEvents.push(group.name); selectedEvents.push(group.name);
@ -119,7 +119,7 @@ export const useLogStreamingStore = defineStore('logStreaming', {
return selectedEvents; return selectedEvents;
}, },
setSelectedInGroup(destinationId: string, name: string, isSelected: boolean) { setSelectedInGroup(destinationId: string, name: string, isSelected: boolean) {
if (destinationId in this.items) { if (this.items[destinationId]) {
const groupName = eventGroupFromEventName(name); const groupName = eventGroupFromEventName(name);
const groupIndex = this.items[destinationId].eventGroups.findIndex( const groupIndex = this.items[destinationId].eventGroups.findIndex(
(e) => e.name === groupName, (e) => e.name === groupName,
@ -166,7 +166,7 @@ export const useLogStreamingStore = defineStore('logStreaming', {
}, },
setSelectionAndBuildItems(destination: MessageEventBusDestinationOptions) { setSelectionAndBuildItems(destination: MessageEventBusDestinationOptions) {
if (destination.id) { if (destination.id) {
if (!(destination.id in this.items)) { if (!this.items[destination.id]) {
this.items[destination.id] = { this.items[destination.id] = {
destination, destination,
selectedEvents: new Set<string>(), selectedEvents: new Set<string>(),