datatracker/cypress/e2e/meeting/agenda-neue.cy.js

1366 lines
64 KiB
JavaScript

import { DateTime } from 'luxon'
import path from 'path'
import { find, first, isEqual, times } from 'lodash-es'
import { faker } from '@faker-js/faker'
import slugify from 'slugify'
import meetingGenerator from '../../generators/meeting'
const xslugify = (str) => slugify(str.replace('/', '-'), { lower: true, strict: true })
const TEST_SEED = 123
const viewports = {
desktop: [1536, 960],
smallDesktop: [1280, 800],
tablet: [768, 1024],
mobile: [360, 760]
}
// Set randomness seed
faker.seed(TEST_SEED)
/**
* Inject meeting info json into the page
*
* @param {*} win Window Object
* @param {*} meetingNumber Meeting Number
*/
function injectMeetingData (win, meetingNumber) {
const meetingDataScript = win.document.createElement('script')
meetingDataScript.id = 'meeting-data'
meetingDataScript.type = 'application/json'
meetingDataScript.innerHTML = `{"meetingNumber": "${meetingNumber}"}`
win.document.querySelector('head').appendChild(meetingDataScript)
}
/**
* Format URL by replacing inline variables
*
* @param {String} url Raw URL
* @param {Object} session Session Object
* @param {String} meetingNumber Meeting Number
* @returns Formatted URL
*/
function formatLinkUrl (url, session, meetingNumber) {
return url ? url.replace('{meeting.number}', meetingNumber)
.replace('{group.acronym}', session.groupAcronym)
.replace('{short}', session.short)
.replace('{order_number}', session.orderInMeeting) : url
}
/**
* Find the first URL in text matching a conference domain
*
* @param {String} txt Raw Text
* @returns First URL found
*/
function findFirstConferenceUrl (txt) {
try {
const fUrl = txt.match(urlRe)
if (fUrl && fUrl[0].length > 0) {
const pUrl = new URL(fUrl[0])
if (conferenceDomains.some(d => pUrl.hostname.endsWith(d))) {
return fUrl[0]
}
}
} catch (err) { }
return null
}
// ====================================================================
// AGENDA-NEUE (past meeting) | DESKTOP viewport
// ====================================================================
describe('meeting -> agenda-neue [past, desktop]', {
viewportWidth: viewports.desktop[0],
viewportHeight: viewports.desktop[1]
}, () => {
let meetingData = null
before(() => {
// Set clock to 2022-02-01 (month is 0-indexed)
cy.clock(new Date(2022, 1, 1))
// Generate meeting data
meetingData = meetingGenerator.generateAgendaResponse({ dateMode: 'past' })
// Intercept Meeting Data API
cy.intercept('GET', `/api/meeting/${meetingData.meeting.number}/agenda-data`, { body: meetingData }).as('getMeetingData')
// Visit agenda page
cy.visit(`/meeting/${meetingData.meeting.number}/agenda-neue`, {
onBeforeLoad: (win) => { injectMeetingData(win, meetingData.meeting.number) }
})
cy.wait('@getMeetingData')
// Fix scroll behavior
// See https://github.com/cypress-io/cypress/issues/3200
cy.document().then(document => {
const htmlElement = document.querySelector('html')
if (htmlElement) {
htmlElement.style.scrollBehavior = 'inherit'
}
})
})
// -> HEADER
it(`has IETF 123 title`, () => {
cy.get('.agenda h1').first().contains(`IETF ${meetingData.meeting.number} Meeting Agenda`)
// Take a snapshot for visual diffing
cy.percySnapshot('meeting -> agenda-neue [past, desktop]', { widths: [viewports.desktop[0]] })
})
it(`has meeting city subtitle`, () => {
cy.get('.agenda h4').first().contains(meetingData.meeting.city)
})
it(`has meeting date subtitle`, () => {
cy.get('.agenda h4').first().contains(/[a-zA-Z] [0-9]{1,2} - ([a-zA-Z]+ )?[0-9]{1,2}, [0-9]{4}/i)
})
it(`has meeting last updated datetime`, () => {
const updatedDateTime = DateTime.fromISO(meetingData.meeting.updated).setZone(meetingData.meeting.timezone).toFormat(`DD 'at' tt ZZZZ`)
cy.get('.agenda h6').first().contains(updatedDateTime)
})
// -> NAV
it(`has the correct navigation items`, () => {
cy.get('.agenda .meeting-nav > li').should('have.length', 3)
cy.get('.agenda .meeting-nav > li').first().contains('Agenda')
cy.get('.agenda .meeting-nav > li').eq(1).contains('Floor plan')
cy.get('.agenda .meeting-nav > li').last().contains('Plaintext')
})
it(`has the Settings button on the right`, () => {
cy.get('.agenda .meeting-nav').next('button').should('exist')
.and('include.text', 'Settings')
cy.window().then(win => {
cy.get('.agenda .meeting-nav').next('button').then(el => {
const btnBounds = el[0].getBoundingClientRect()
expect(btnBounds.x).to.be.greaterThan(win.innerWidth - btnBounds.width - 100)
})
})
})
// -> SCHEDULE LIST -> Header
it(`has schedule list title`, () => {
cy.get('.agenda h2').first().contains(`Schedule`)
})
it(`has info note`, () => {
cy.get('.agenda .agenda-infonote').should('exist').and('include.text', meetingData.meeting.infoNote)
})
it(`info note can be dismissed / reopened`, () => {
cy.get('.agenda .agenda-infonote > button').click()
cy.get('.agenda .agenda-infonote').should('not.exist')
cy.get('.agenda h2').first().next('button').should('exist')
cy.get('.agenda h2').first().next('button').click()
cy.get('.agenda .agenda-infonote').should('exist')
cy.get('.agenda h2').first().next('button').should('not.exist')
})
it(`has timezone selector`, () => {
cy.get('.agenda .agenda-tz-selector').should('exist')
cy.get('.agenda .agenda-tz-selector').prev().should('exist').and('include.text', 'Timezone:').prev('.bi').should('exist')
cy.get('.agenda .agenda-tz-selector > button').should('have.length', 3)
cy.get('.agenda .agenda-tz-selector > button').first().contains('Meeting')
cy.get('.agenda .agenda-tz-selector > button').eq(1).contains('Local')
cy.get('.agenda .agenda-tz-selector > button').last().contains('UTC')
cy.get('.agenda .agenda-timezone-ddn').should('exist')
})
it.skip('can change timezone', () => {
// Switch to local timezone
cy.get('.agenda .agenda-tz-selector > button').eq(1).click().should('have.class', 'n-button--primary-type')
.prev('button').should('not.have.class', 'n-button--primary-type')
const localDateTime = DateTime.fromISO(meetingData.meeting.updated).setZone('local').toFormat(`DD 'at' tt ZZZZ`)
cy.get('.agenda h6').first().contains(localDateTime)
// Switch to UTC
cy.get('.agenda .agenda-tz-selector > button').last().click().should('have.class', 'n-button--primary-type')
.prev('button').should('not.have.class', 'n-button--primary-type')
const utcDateTime = DateTime.fromISO(meetingData.meeting.updated).setZone('utc').toFormat(`DD 'at' tt ZZZZ`)
cy.get('.agenda h6').first().contains(utcDateTime)
cy.get('.agenda .agenda-timezone-ddn').contains('UTC')
// Switch back to meeting timezone
cy.get('.agenda .agenda-tz-selector > button').first().click().should('have.class', 'n-button--primary-type')
cy.get('.agenda .agenda-timezone-ddn').contains('Tokyo')
})
// -> SCHEDULE LIST -> Table Headers
it('has schedule list table headers', () => {
// Table Headers
cy.get('.agenda-table-head-time').should('exist').and('contain', 'Time')
cy.get('.agenda-table-head-location').should('exist').and('contain', 'Location')
cy.get('.agenda-table-head-event').should('exist').and('contain', 'Event')
// Day Headers
cy.get('.agenda-table-display-day').should('have.length', 7).each((el, idx) => {
const localDateTime = DateTime.fromISO(meetingData.meeting.startDate).setZone('local').plus({ days: idx }).toLocaleString(DateTime.DATE_HUGE)
cy.wrap(el).should('contain', localDateTime)
})
})
// -> SCHEDULE LIST -> Table Events
it('has schedule list table events (can take a while)', {
// This test is VERY memory-intensive, so disable DOM snapshots to prevent browser crash
numTestsKeptInMemory: 0
}, () => {
let isFirstSession = true
cy.get('tr.agenda-table-display-event').should('have.length', meetingData.schedule.length).each((el, idx) => {
// Apply small arbitrary wait every 10 rows to prevent the test UI from freezing
if (idx % 10 === 0) {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10)
}
const event = meetingData.schedule[idx]
const eventStart = DateTime.fromISO(event.startDateTime)
const eventEnd = eventStart.plus({ seconds: event.duration })
const eventTimeSlot = `${eventStart.toFormat('HH:mm')} - ${eventEnd.toFormat('HH:mm')}`
// --------
// Location
// --------
if (event.location?.short) {
// Has floor badge
cy.wrap(el).find('.agenda-table-cell-room > a').should('contain', event.room)
.and('have.attr', 'href', `/meeting/` + meetingData.meeting.number + `/floor-plan-neue?room=` + xslugify(event.room))
.prev('.badge').should('contain', event.location.short)
} else {
// No floor badge
cy.wrap(el).find('.agenda-table-cell-room > span:not(.badge)').should('contain', event.room)
.prev('.badge').should('not.exist')
}
// ---------------------------------------------------
// Type-specific timeslot / group / name columns tests
// ---------------------------------------------------
if (event.type === 'regular') {
// First session should have header row above it
if (isFirstSession) {
cy.wrap(el).prev('tr.agenda-table-display-session-head').should('exist')
.find('.agenda-table-cell-ts').should('contain', eventTimeSlot)
.next('.agenda-table-cell-name').should('contain', `${DateTime.fromISO(event.startDateTime).toFormat('cccc')} ${event.name}`)
}
// Timeslot
cy.wrap(el).find('.agenda-table-cell-ts').should('contain', '—')
// Group Acronym + Parent
cy.wrap(el).find('.agenda-table-cell-group > .badge').should('contain', event.groupParent.acronym)
.next('a').should('contain', event.acronym).and('have.attr', 'href', `/group/` + event.acronym + `/about/`)
// Group Name
cy.wrap(el).find('.agenda-table-cell-name').should('contain', event.groupName)
isFirstSession = false
} else {
// Timeslot
cy.wrap(el).find('.agenda-table-cell-ts').should('contain', eventTimeSlot)
// Event Name
cy.wrap(el).find('.agenda-table-cell-name').should('contain', event.name)
isFirstSession = true
}
// -----------
// Name column
// -----------
// Event icon
if (['break', 'plenary'].includes(event.type) || (event.type === 'other' && ['office hours', 'hackathon'].some(s => event.name.toLowerCase().indexOf(s) >= 0))) {
cy.wrap(el).find('.agenda-table-cell-name > i.bi').should('exist')
}
// Name link
if (event.flags.agenda) {
cy.wrap(el).find('.agenda-table-cell-name > a').should('have.attr', 'href', event.agenda.url)
}
// BoF badge
if (event.isBoF) {
cy.wrap(el).find('.agenda-table-cell-name > .badge').should('contain', 'BoF')
}
// Note
if (event.note) {
cy.wrap(el).find('.agenda-table-cell-name > .agenda-table-note').should('exist')
.find('i.bi').should('exist')
.next('span').should('contain', event.note)
}
// -----------------------
// Buttons / Status Column
// -----------------------
switch (event.status) {
// Cancelled
case 'canceled': {
cy.wrap(el).find('.agenda-table-cell-links > .badge.is-cancelled').should('contain', 'Cancelled')
break
}
// Rescheduled
case 'resched': {
cy.wrap(el).find('.agenda-table-cell-links > .badge.is-rescheduled').should('contain', 'Rescheduled')
break
}
// Scheduled
case 'sched': {
if (event.flags.showAgenda || ['regular', 'plenary'].includes(event.type)) {
cy.wrap(el).find('.agenda-table-cell-links > .agenda-table-cell-links-buttons').as('eventbuttons')
if (event.flags.agenda) {
// Show meeting materials button
cy.get('@eventbuttons').find('i.bi.bi-collection').should('exist')
// ZIP materials button
cy.get('@eventbuttons').find(`#btn-lnk-${event.id}-tar`).should('have.attr', 'href', `/meeting/${meetingData.meeting.number}/agenda/${event.acronym}-drafts.tgz`)
.children('i.bi').should('exist')
// PDF materials button
cy.get('@eventbuttons').find(`#btn-lnk-${event.id}-pdf`).should('have.attr', 'href', `/meeting/${meetingData.meeting.number}/agenda/${event.acronym}-drafts.pdf`)
.children('i.bi').should('exist')
} else if (event.type === 'regular') {
// No meeting materials yet warning badge
cy.get('@eventbuttons').find('.no-meeting-materials').should('exist')
}
// Notepad button
const hedgeDocLink = `https://notes.ietf.org/notes-ietf-${meetingData.meeting.number}-${event.type === 'plenary' ? 'plenary' : event.acronym}`
cy.get('@eventbuttons').find(`#btn-lnk-${event.id}-note`).should('have.attr', 'href', hedgeDocLink)
.children('i.bi').should('exist')
// Chat logs
cy.get('@eventbuttons').find(`#btn-lnk-${event.id}-logs`).should('have.attr', 'href', event.links.chatArchive)
.children('i.bi').should('exist')
// Recordings
for (const rec of event.links.recordings) {
if (rec.url.indexOf('audio') > 0) {
// -> Audio
cy.get('@eventbuttons').find(`#btn-lnk-${event.id}-audio-${rec.id}`).should('have.attr', 'href', rec.url)
.children('i.bi').should('exist')
} else if (rec.url.indexOf('youtu') > 0) {
// -> Youtube
cy.get('@eventbuttons').find(`#btn-lnk-${event.id}-youtube-${rec.id}`).should('have.attr', 'href', rec.url)
.children('i.bi').should('exist')
} else {
// -> Others
cy.get('@eventbuttons').find(`#btn-lnk-${event.id}-video-${rec.id}`).should('have.attr', 'href', rec.url)
.children('i.bi').should('exist')
}
}
// Video Stream
if (event.links.videoStream) {
const videoStreamLink = `https://www.meetecho.com/ietf${meetingData.meeting.number}/recordings#${event.acronym.toUpperCase()}`
cy.get('@eventbuttons').find(`#btn-lnk-${event.id}-rec`).should('have.attr', 'href', videoStreamLink)
.children('i.bi').should('exist')
}
} else {
cy.wrap(el).find('.agenda-table-cell-links > .agenda-table-cell-links-buttons').should('not.exist')
}
break
}
}
})
})
// -> SCHEDULE LIST -> Search
it('can search meetings', {
// No need to keep DOM snapshots for this test
numTestsKeptInMemory: 0
}, () => {
cy.get('.agenda-table > .agenda-table-search > button').click()
cy.get('.agenda-search').should('exist').and('be.visible')
const event = find(meetingData.schedule, s => s.type === 'regular')
const eventWithNote = find(meetingData.schedule, s => s.note)
// Search different terms
const searchTerms = [
'hack', // Should match hackathon events
event.groupAcronym, // Match group name
event.room.toLowerCase(), // Match room name
eventWithNote.note.substring(0, 10).toLowerCase() // Match partial note
]
for (const term of searchTerms) {
cy.get('.agenda-search input[type=text]').clear().type(term)
cy.get('.agenda-table .agenda-table-display-event').should('have.length.lessThan', meetingData.schedule.length)
// Let the UI update before checking each displayed row
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000)
cy.get('.agenda-table .agenda-table-display-event').each((el, idx) => {
cy.wrap(el).contains(term, { matchCase: false })
})
}
// Clear button
cy.get('.agenda-search button').click()
cy.get('.agenda-search input[type=text]').should('have.value', '')
cy.get('.agenda-table .agenda-table-display-event').should('have.length', meetingData.schedule.length)
// Invalid search
cy.get('.agenda-search input[type=text]').type(faker.vehicle.vin())
cy.get('.agenda-table .agenda-table-display-event').should('have.length', 0)
cy.get('.agenda-table .agenda-table-display-noresult').should('exist').and('contain', 'No event matching your search query.')
// Closing search should clear search
cy.get('.agenda-table > .agenda-table-search > button').click()
cy.get('.agenda-search').should('not.exist')
cy.get('.agenda-table .agenda-table-display-event').should('have.length', meetingData.schedule.length)
})
// -> SCHEDULE LIST -> Show Meeting Materials dialog
it('can show meeting materials dialog', () => {
const event = find(meetingData.schedule, s => s.flags.showAgenda && s.flags.agenda)
const eventStart = DateTime.fromISO(event.startDateTime)
const eventEnd = eventStart.plus({ seconds: event.duration })
// Intercept meeting materials request
const materialsUrl = (new URL(event.agenda.url)).pathname
const materialsInfo = {
url: event.agenda.url,
slides: times(5, idx => ({
id: 100000 + idx,
title: faker.commerce.productName(),
url: `/meeting/${meetingData.meeting.number}/materials/slides-${meetingData.meeting.number}-${event.acronym}-${faker.internet.domainWord()}`,
ext: ['pdf', 'html', 'md', 'txt', 'pptx'][idx]
})),
minutes: {
ext: 'md',
id: 123456,
title: 'Minutes IETF123 Testing',
url: `/meeting/${meetingData.meeting.number}/materials/minutes-${meetingData.meeting.number}-${event.acronym}-${faker.internet.domainWord()}`
}
}
cy.intercept('GET', `/api/meeting/session/${event.sessionId}/materials`, { body: materialsInfo }).as('getMaterialsInfo')
cy.intercept('GET', materialsUrl, { body: 'The internet is a series of tubes.' }).as('getMaterialsText')
cy.intercept('GET', materialsInfo.minutes.url, { body: 'One does not simply walk into mordor.' }).as('getMaterialsMinutes')
// Open dialog
cy.get(`#agenda-rowid-${event.id}`).find(`#btn-lnk-${event.id}-mat`).click()
cy.get('.agenda-eventdetails').should('exist').and('be.visible')
cy.wait('@getMaterialsText')
// Header
cy.get('.agenda-eventdetails .n-card-header__main > .detail-header > .bi').should('exist')
.next('span').should('contain', eventStart.toFormat('DDDD'))
cy.get('.agenda-eventdetails .n-card-header__extra > .detail-header > .bi').should('exist')
.next('strong').should('contain', `${eventStart.toFormat('T')} - ${eventEnd.toFormat('T')}`)
cy.get('.agenda-eventdetails .detail-title > h6 > .bi').should('exist')
.next('span').should('contain', event.name)
cy.get('.agenda-eventdetails .detail-location > .bi').should('exist')
.next('.badge').should('contain', event.location.short)
.next('span').should('contain', event.room)
// Navigation
cy.get('.agenda-eventdetails .detail-nav > a').should('have.length', 3)
.first().should('have.class', 'active')
.nextAll().should('not.have.class', 'active')
// Agenda Tab
cy.get('.agenda-eventdetails .detail-text > iframe').should('have.attr', 'src', materialsUrl)
// Slides Tab
cy.get('.agenda-eventdetails .detail-nav > a').eq(1).click()
.should('have.class', 'active')
.siblings('a').should('not.have.class', 'active')
cy.get('.agenda-eventdetails .detail-text > .list-group > .list-group-item').should('have.length', materialsInfo.slides.length).each((el, idx) => {
cy.wrap(el).should('have.attr', 'href', materialsInfo.slides[idx].url)
.children('.bi').should('have.class', `bi-filetype-${materialsInfo.slides[idx].ext}`)
.next('span').should('contain', materialsInfo.slides[idx].title)
})
// Minutes Tab
cy.get('.agenda-eventdetails .detail-nav > a').eq(2).click()
.should('have.class', 'active')
.prevAll('a').should('not.have.class', 'active')
cy.wait('@getMaterialsMinutes')
cy.get('.agenda-eventdetails .detail-text > iframe').should('have.attr', 'src', materialsInfo.minutes.url)
// Footer Buttons
const hedgeDocLink = `https://notes.ietf.org/notes-ietf-${meetingData.meeting.number}-${event.type === 'plenary' ? 'plenary' : event.acronym}`
cy.get('.agenda-eventdetails .detail-action > a').should('have.length', 3)
.first().should('contain', 'Download as tarball').should('have.attr', 'href', `/meeting/${meetingData.meeting.number}/agenda/${event.acronym}-drafts.tgz`)
.next().should('contain', 'Download as PDF').should('have.attr', 'href', `/meeting/${meetingData.meeting.number}/agenda/${event.acronym}-drafts.pdf`)
.next().should('contain', 'Notepad').should('have.attr', 'href', hedgeDocLink)
// Clicking X should close the dialog
cy.get('.agenda-eventdetails .n-card-header__extra > .detail-header > button').click()
})
// -> SCHEDULE LIST -> Show Meeting Materials dialog (EMPTY VARIANT)
it('can show meeting materials dialog (empty variant)', () => {
const event = find(meetingData.schedule, s => s.flags.showAgenda && s.flags.agenda)
// Intercept meeting materials request
const materialsUrl = (new URL(event.agenda.url)).pathname
const materialsInfo = {
url: event.agenda.url,
slides: [],
minutes: null
}
cy.intercept('GET', `/api/meeting/session/${event.sessionId}/materials`, { body: materialsInfo }).as('getMaterialsInfo')
cy.intercept('GET', materialsUrl, { body: 'The internet is a series of tubes.' }).as('getMaterialsText')
// Open dialog
cy.get(`#agenda-rowid-${event.id}`).find(`#btn-lnk-${event.id}-mat`).click()
cy.get('.agenda-eventdetails').should('exist').and('be.visible')
cy.wait('@getMaterialsText')
// Slides Tab
cy.get('.agenda-eventdetails .detail-nav > a').eq(1).click()
cy.get('.agenda-eventdetails .detail-text').should('contain', 'No slides submitted for this session.')
// Minutes Tab
cy.get('.agenda-eventdetails .detail-nav > a').eq(2).click()
cy.get('.agenda-eventdetails .detail-text').should('contain', 'No minutes submitted for this session.')
// Clicking X should close the dialog
cy.get('.agenda-eventdetails .n-card-header__extra > .detail-header > button').click()
})
// -> FILTER BY AREA/GROUP DIALOG
it('can filter by area/group', {
// This test has lot of UI element interactions and the UI can get slow with DOM snapshots, so disable it
numTestsKeptInMemory: 0
}, () => {
// Open dialog
cy.get('#agenda-quickaccess-filterbyareagroups-btn').should('exist').and('be.visible').click()
cy.get('.agenda-personalize').should('exist').and('be.visible')
// Check header elements
cy.get('.agenda-personalize .n-drawer-header__main > span').contains('Filter Areas + Groups')
cy.get('.agenda-personalize .agenda-personalize-actions > button').should('have.length', 3)
cy.get('.agenda-personalize .agenda-personalize-actions > button').first().contains('Clear Selection')
cy.get('.agenda-personalize .agenda-personalize-actions > button').eq(1).contains('Cancel')
cy.get('.agenda-personalize .agenda-personalize-actions > button').last().contains('Apply')
// Check categories
cy.get('.agenda-personalize .agenda-personalize-category').should('have.length', meetingData.categories.length)
// Check areas + groups
cy.get('.agenda-personalize .agenda-personalize-category').each((el, idx) => {
const cat = meetingData.categories[idx]
cy.wrap(el).find('.agenda-personalize-area').should('have.length', cat.length)
.each((areaEl, areaIdx) => {
// Area Button
const area = cat[areaIdx]
cy.wrap(areaEl).find('.agenda-personalize-areamain').scrollIntoView()
if (area.label) {
cy.wrap(areaEl).find('.agenda-personalize-areamain > button').should('be.visible').contains(area.label)
} else {
cy.wrap(areaEl).find('.agenda-personalize-areamain > button').should('not.exist')
}
// Group Buttons
cy.wrap(areaEl).find('.agenda-personalize-groups > button').should('have.length', area.children.length)
.each((groupEl, groupIdx) => {
const group = area.children[groupIdx]
cy.wrap(groupEl).should('be.visible').contains(group.label)
if (group.is_bof) {
cy.wrap(groupEl).should('have.class', 'is-bof')
cy.wrap(groupEl).find('.badge').should('be.visible').contains('BoF')
}
})
// Test Area Selection
if (area.label) {
cy.wrap(areaEl).find('.agenda-personalize-areamain > button').click()
cy.wrap(areaEl).find('.agenda-personalize-groups > button').should('have.class', 'is-checked')
cy.wrap(areaEl).find('.agenda-personalize-areamain > button').click()
cy.wrap(areaEl).find('.agenda-personalize-groups > button').should('not.have.class', 'is-checked')
}
// Test Group Selection
cy.wrap(areaEl).find('.agenda-personalize-groups > button').any().click()
.should('have.class', 'is-checked').click().should('not.have.class', 'is-checked')
})
})
// Test multi-toggled_by button trigger
cy.get(`.agenda-personalize .agenda-personalize-category:last .agenda-personalize-area:last .agenda-personalize-groups > button:contains('BoF')`).as('bofbtn')
cy.get('@bofbtn').click()
cy.get('.agenda-personalize .agenda-personalize-group:has(.badge)').should('have.class', 'is-checked')
cy.get('@bofbtn').click()
cy.get('.agenda-personalize .agenda-personalize-group:has(.badge)').should('not.have.class', 'is-checked')
// Clicking all groups from area then area button should unselect all
cy.get('.agenda-personalize .agenda-personalize-area:first .agenda-personalize-groups > button').click({ multiple: true })
cy.get('.agenda-personalize .agenda-personalize-area:first .agenda-personalize-areamain > button').click()
cy.get('.agenda-personalize .agenda-personalize-area:first .agenda-personalize-groups > button').should('not.have.class', 'is-checked')
// Test Clear Selection
cy.get('.agenda-personalize .agenda-personalize-group').any(10).click({ multiple: true })
cy.get('.agenda-personalize .agenda-personalize-actions > button').first().click()
cy.get('.agenda-personalize .agenda-personalize-group').should('not.have.class', 'is-checked')
// Click Cancel should hide dialog
cy.get('.agenda-personalize .agenda-personalize-actions > button').eq(1).click()
cy.get('.agenda-personalize').should('not.exist')
})
// -> PICK SESSIONS
it('can pick individual sessions', () => {
// Enter pick mode
cy.get('#agenda-quickaccess-picksessions-btn').should('be.visible').click().should('not.exist')
cy.get('#agenda-quickaccess-applypick-btn').should('be.visible')
cy.get('#agenda-quickaccess-discardpick-btn').should('be.visible')
// Pick 10 random sessions
cy.get('.agenda .agenda-table-cell-check > .n-checkbox').should('have.length', meetingData.schedule.length)
.any(10).click({ multiple: true })
cy.get('#agenda-quickaccess-applypick-btn').click().should('not.exist')
cy.get('#agenda-quickaccess-modifypick-btn').should('be.visible')
cy.get('#agenda-quickaccess-discardpick-btn').should('be.visible')
cy.get('.agenda .agenda-table-display-event').should('have.length', 10)
// Change selection (keep existing 5 + add 5 new ones)
cy.get('#agenda-quickaccess-modifypick-btn').click().should('not.exist')
cy.get('#agenda-quickaccess-applypick-btn').should('be.visible')
cy.get('#agenda-quickaccess-discardpick-btn').should('be.visible')
cy.get('.agenda .agenda-table-cell-check > .n-checkbox').should('have.length', meetingData.schedule.length)
.filter('.n-checkbox--checked').should('have.length', 10)
.take(5).click({ multiple: true })
cy.get('.agenda .agenda-table-cell-check > .n-checkbox:not(.n-checkbox--checked)').any(5).click({ multiple: true })
cy.get('#agenda-quickaccess-applypick-btn').click()
cy.get('.agenda .agenda-table-display-event').should('have.length', 10)
// Discard should clear selection
cy.get('#agenda-quickaccess-discardpick-btn').click().should('not.exist')
cy.get('#agenda-quickaccess-modifypick-btn').should('not.exist')
cy.get('#agenda-quickaccess-picksessions-btn').should('be.visible')
cy.get('.agenda .agenda-table-cell-check').should('not.exist')
cy.get('.agenda .agenda-table-display-event').should('have.length', meetingData.schedule.length)
})
// -> CALENDAR VIEW
it('can view calendar', () => {
// Open dialog
cy.get('#agenda-quickaccess-calview-btn').should('be.visible').click()
cy.get('.agenda-calendar').should('exist').and('be.visible')
// Check header elements
cy.get('.agenda-calendar .n-drawer-header__main > span').contains('Calendar View')
cy.get('.agenda-calendar .agenda-calendar-actions').as('diagheader')
cy.get('@diagheader').children('button').should('have.length', 2)
cy.get('@diagheader').children('button').first().should('include.text', 'Filter')
cy.get('@diagheader').children('button').last().should('include.text', 'Close')
// -----------------------
// Check timezone controls
// -----------------------
// cy.get('@diagheader').children('small').first().should('contain', 'Timezone')
// // Switch to local timezone
// cy.get('@diagheader').children('.n-button-group').find('button').as('tzbuttons').eq(1).click().should('have.class', 'n-button--primary-type')
// .prev('button').should('not.have.class', 'n-button--primary-type')
// const localDateTime = DateTime.fromISO(meetingData.meeting.updated).setZone('local').toFormat(`DD 'at' tt ZZZZ`)
// cy.get('.agenda h6').first().contains(localDateTime)
// // Switch to UTC
// cy.get('@tzbuttons').last().click().should('have.class', 'n-button--primary-type')
// .prev('button').should('not.have.class', 'n-button--primary-type')
// const utcDateTime = DateTime.fromISO(meetingData.meeting.updated).setZone('utc').toFormat(`DD 'at' tt ZZZZ`)
// cy.get('.agenda h6').first().contains(utcDateTime)
// // Switch back to meeting timezone
// cy.get('@tzbuttons').first().click().should('have.class', 'n-button--primary-type')
// ----------------------
// Check Filters Shortcut
// ----------------------
cy.get('@diagheader').children('button').first().click()
// Only check whether the dialog is shown. We already tested the dialog earlier.
cy.get('.agenda-personalize').should('be.visible')
// Close dialog
cy.get('.agenda-personalize .agenda-personalize-actions > button').eq(1).click()
cy.get('.agenda-personalize').should('not.exist')
// ------------------
// Check Event Dialog
// ------------------
const firstEvent = meetingData.schedule[0]
const materialsUrl = (new URL(firstEvent.agenda.url)).pathname
const materialsInfo = {
url: firstEvent.agenda.url,
slides: [],
minutes: null
}
cy.intercept('GET', `/api/meeting/session/${firstEvent.sessionId}/materials`, { body: materialsInfo }).as('getMaterialsInfo')
cy.intercept('GET', materialsUrl, { body: 'The internet is a series of tubes.' }).as('getMaterialsText')
cy.get('.agenda-calendar .fc-event').first().click()
// Only check whether the dialog is shown. We already tested the dialog earlier.
cy.get('.agenda-eventdetails').should('be.visible')
// Close dialog
cy.get('.agenda-eventdetails .n-card-header__extra > .detail-header > button').click()
// -----------
// Event Hover
// -----------
// First Event
let eventStart = DateTime.fromISO(firstEvent.startDateTime)
let eventEnd = eventStart.plus({ seconds: firstEvent.duration })
let hoverDateTime = `${eventStart.toFormat('DDDD')} from ${eventStart.toFormat('T')} to ${eventEnd.toFormat('T')}`
cy.get('.agenda-calendar .fc-event').first().realHover({ position: 'center' })
cy.get('.agenda-calendar-hint > div').first().should('include.text', firstEvent.name)
.next().should('include.text', firstEvent.location.short).and('include.text', firstEvent.room)
.next().should('include.text', hoverDateTime)
// Second Event
const secondEvent = meetingData.schedule[1]
eventStart = DateTime.fromISO(secondEvent.startDateTime)
eventEnd = eventStart.plus({ seconds: secondEvent.duration })
hoverDateTime = `${eventStart.toFormat('DDDD')} from ${eventStart.toFormat('T')} to ${eventEnd.toFormat('T')}`
cy.get('.agenda-calendar .fc-event').eq(1).realHover({ position: 'center' })
cy.get('.agenda-calendar-hint > div').first().should('include.text', secondEvent.name)
.next().should('include.text', secondEvent.location.short).and('include.text', secondEvent.room)
.next().should('include.text', hoverDateTime)
// ------------------------------
// Click Close should hide dialog
// ------------------------------
cy.get('@diagheader').children('button').last().click()
cy.get('.agenda-calendar').should('not.exist')
})
// -> SETTINGS DIALOG
it('can change settings', () => {
// Open dialog
cy.get('.meeting-nav').next('button').should('exist').and('be.visible').click()
cy.get('.agenda-settings').should('exist').and('be.visible')
// Check header elements
cy.get('.agenda-settings .n-drawer-header__main > span').contains('Agenda Settings')
cy.get('.agenda-settings .agenda-settings-actions > button').should('have.length', 2)
cy.get('.agenda-settings .agenda-settings-actions > button').first().should('be.visible')
cy.get('.agenda-settings .agenda-settings-actions > button').last().contains('Close')
// -------------------
// Check export config
// -------------------
cy.get('.agenda-settings .agenda-settings-actions > button').first().click()
cy.get('.n-dropdown-option:contains("Export Configuration")').should('exist').and('be.visible').click()
cy.readFile(path.join(Cypress.config('downloadsFolder'), 'agenda-settings.json'), { timeout: 15000 }).then(cfg => {
cy.fixture('agenda-settings.json').then(cfgValid => {
expect(isEqual(cfg, cfgValid)).to.be.true
})
})
// -------------------
// Check import config
// -------------------
// Skip test if firefox/safari since they don't support the file picker API
if (!Cypress.isBrowser('firefox') && !Cypress.isBrowser('safari')) {
cy.fixture('agenda-settings.json', { encoding: 'utf8' }).then(cfgImport => {
// Stub the native file picker
// From https://cypresstips.substack.com/p/stub-the-browser-filesystem-api
cy.window().then((win) => {
cy.stub(win, 'showOpenFilePicker').resolves([{
getFile: cy.stub().resolves({
text: cy.stub().resolves(JSON.stringify(cfgImport))
})
}])
cy.get('.agenda-settings .agenda-settings-actions > button').first().click()
cy.get('.n-dropdown-option:contains("Import Configuration")').should('exist').and('be.visible').click()
cy.get('.n-message').should('contain', 'Config imported successfully')
})
})
} else {
cy.log('Config import test skipped because this browser does not support file picker API, which is required for the test.')
}
// -----------------------
// Check timezone controls
// -----------------------
// cy.get('.agenda-settings-content > .n-divider').first().should('contain', 'Timezone').as('settings-timezone')
// // Switch to local timezone
// cy.get('@settings-timezone').next('.n-button-group').find('button').eq(1).click().should('have.class', 'n-button--primary-type')
// .prev('button').should('not.have.class', 'n-button--primary-type')
// const localDateTime = DateTime.fromISO(meetingData.meeting.updated).setZone('local').toFormat(`DD 'at' tt ZZZZ`)
// cy.get('.agenda h6').first().contains(localDateTime)
// // Switch to UTC
// cy.get('@settings-timezone').next('.n-button-group').find('button').last().click().should('have.class', 'n-button--primary-type')
// .prev('button').should('not.have.class', 'n-button--primary-type')
// const utcDateTime = DateTime.fromISO(meetingData.meeting.updated).setZone('utc').toFormat(`DD 'at' tt ZZZZ`)
// cy.get('.agenda h6').first().contains(utcDateTime)
// // Switch back to meeting timezone
// cy.get('@settings-timezone').next('.n-button-group').find('button').first().click().should('have.class', 'n-button--primary-type')
// cy.get('@settings-timezone').next('.n-button-group').next('.n-select').contains('Tokyo')
// ----------------------
// Check display controls
// ----------------------
cy.get('.agenda-settings-content > .n-divider').eq(1).should('contain', 'Display').as('settings-display')
// -> Test Current Meeting Info Note toggle
cy.get('@settings-display').nextAll('div.d-flex').eq(1).find('div[role=switch]').as('switch-infonote').click()
cy.get('.agenda .agenda-infonote').should('not.exist')
cy.get('@switch-infonote').click()
cy.get('.agenda .agenda-infonote').should('exist')
// -> Test Event Icons toggle
cy.get('@settings-display').nextAll('div.d-flex').eq(2).find('div[role=switch]').as('switch-eventicons').click()
cy.get('.agenda .agenda-event-icon').should('not.exist')
cy.get('@switch-eventicons').click()
cy.get('.agenda .agenda-event-icon').should('exist')
// -> Test Floor Indicators toggle
cy.get('@settings-display').nextAll('div.d-flex').eq(3).find('div[role=switch]').as('switch-floorind').click()
cy.get('.agenda .agenda-table-cell-room > span.badge').should('not.exist')
cy.get('@switch-floorind').click()
cy.get('.agenda .agenda-table-cell-room > span.badge').should('exist')
// -> Test Group Area Indicators toggle
cy.get('@settings-display').nextAll('div.d-flex').eq(4).find('div[role=switch]').as('switch-groupind').click()
cy.get('.agenda .agenda-table-cell-group > span.badge').should('not.exist')
cy.get('@switch-groupind').click()
cy.get('.agenda .agenda-table-cell-group > span.badge').should('exist')
// TODO: realtime red line toggle
// -> Test Bolder Text toggle
cy.get('@settings-display').nextAll('div.d-flex').eq(6).find('div[role=switch]').as('switch-boldertext').click()
cy.get('.agenda').should('have.class', 'bolder-text')
cy.get('@switch-boldertext').click()
cy.get('.agenda').should('not.have.class', 'bolder-text')
// ----------------------------
// Check calendar view controls
// ----------------------------
cy.get('.agenda-settings-content > .n-divider').eq(2).should('contain', 'Calendar View').as('settings-calendar')
// TODO: calendar view checks
// ----------------------------
// Check calendar view controls
// ----------------------------
cy.get('.agenda-settings-content > .n-divider').eq(3).should('contain', 'Custom Colors / Tags').as('settings-colors')
// ------------------------------
// Click Close should hide dialog
// ------------------------------
cy.get('.agenda-settings .agenda-settings-actions > button').last().click()
cy.get('.agenda-settings').should('not.exist')
})
// -> ADD TO CALENDAR
it('can add to calendar', () => {
cy.get('#agenda-quickaccess-addtocal-btn').should('be.visible').and('include.text', 'Add to your calendar').click()
cy.get('.n-dropdown-menu > .n-dropdown-option').should('have.length', 2)
.first().should('include.text', 'Subscribe')
.next().should('include.text', 'Download')
// Cannot test if .ics download works because of cypress bug:
// See https://github.com/cypress-io/cypress/issues/14857
// // Intercept Download ICS Call
// cy.intercept('GET', `/meeting/${meetingData.meeting.number}/agenda.ics`, {
// body: 'test',
// headers: {
// 'Content-disposition': 'attachment; filename=agenda.ics',
// 'Content-Type': 'text/calendar'
// }
// }).as('getIcs')
// // Test Download ICS
// cy.get('.n-dropdown-menu > .n-dropdown-option').eq(1).click()
// cy.wait('@getIcs')
})
// -> JUMP TO DAY
it(`can jump to specific days`, () => {
// -> Separator label
cy.get('.agenda .agenda-quickaccess-jumpto').prev('div[role=separator]').should('be.visible').and('include.text', 'Jump to...')
// -> Check nav items
cy.get('.agenda .agenda-quickaccess-jumpto > .nav-item').should('have.length', 7).as('dayjumpbuttons')
.each((el, idx) => {
const localDateTime = DateTime.fromISO(meetingData.meeting.startDate).setZone('local').plus({ days: idx }).toLocaleString(DateTime.DATE_HUGE)
cy.wrap(el).should('contain', localDateTime)
})
// Scroll to last day
// Cypress does not handle the IntersectionObserver correctly, so disable this test for now.
// See https://github.com/cypress-io/cypress/issues/3848
cy.get('@dayjumpbuttons').last().children('a').click({ scrollBehavior: false, force: true }) // .should('have.class', 'active')
cy.get('.agenda-table-display-day').last().isInViewport()
// Scroll to second day
cy.get('@dayjumpbuttons').eq(1).children('a').click({ scrollBehavior: false, force: true }) // .should('have.class', 'active')
cy.get('.agenda-table-display-day').eq(1).isInViewport()
cy.scrollTo('top')
})
// -> Color Tagging
it(`can assign colors/tags to sessions`, () => {
cy.scrollTo('top')
cy.get('.agenda .agenda-table-colorpicker').should('be.visible').click({ scrollBehavior: false })
// Check Legend
cy.get('.agenda .agenda-colorlegend').should('be.visible')
.children().first().should('include.text', 'Color Legend')
.nextAll().should('have.length', 5)
// Check color dots
cy.get('.agenda .agenda-table-display-event .agenda-table-colorindicator.is-active').should('have.length', meetingData.schedule.length)
// -------------------------
// Assign colors to sessions
// -------------------------
cy.get('.agenda .agenda-table-display-event').take(5).each((el, idx) => {
cy.wrap(el).find('.agenda-table-colorindicator').should('be.visible').click({ scrollBehavior: false, force: true })
.prev('.agenda-table-colorchoices').should('be.visible')
.children('.agenda-table-colorchoice').should('have.length', 6)
.eq(idx + 1).click({ scrollBehavior: false }).should('not.exist')
})
// Exit color assignment mode
cy.get('.agenda .agenda-table-colorpicker').click({ scrollBehavior: false })
cy.get('.agenda .agenda-table-display-event .agenda-table-colorindicator').should('have.length', 5).and('not.have.class', 'is-active')
cy.get('.agenda .agenda-colorlegend').should('be.visible')
// ----------------------------------------
// Change color legend from settings dialog
// ----------------------------------------
// Open dialog
cy.get('.meeting-nav').next('button').should('exist').and('be.visible').click()
cy.get('.agenda-settings').should('exist').and('be.visible')
// Toggle color legend switch
cy.get('.agenda-settings-content > .n-divider').eq(1).should('contain', 'Display')
.next('div.d-flex').find('div[role=switch]').as('switch-colorlegend').click()
// Legend should be hidden
cy.get('.agenda .agenda-colorlegend').should('not.exist')
// Toggle color legend back
cy.get('@switch-colorlegend').click()
// Legend should be visible
cy.get('.agenda .agenda-colorlegend').should('be.visible')
// Change color names
cy.get('#agenda-settings-colors-header').nextAll('div.d-flex').each((el, idx) => {
const newName = faker.music.genre()
cy.wrap(el).find('.n-input').clear().type(newName)
// TODO: Color names + values don't update in test mode for some reason... Watcher not triggering? Skipped for now.
// cy.get('.agenda .agenda-colorlegend').children().eq(idx + 1).should('include.text', newName)
})
// Close dialog
cy.get('.agenda-settings .agenda-settings-actions > button').last().click()
cy.get('.agenda-settings').should('not.exist')
// ---------------
// Unassign colors
// ---------------
// Re-enter color assignment mode
cy.get('.agenda .agenda-table-colorpicker').should('be.visible').click({ scrollBehavior: false })
// Remove color selection
cy.get('.agenda .agenda-table-display-event').take(5).each((el, idx) => {
cy.wrap(el).find('.agenda-table-colorindicator').should('be.visible').click({ scrollBehavior: false, force: true })
.prev('.agenda-table-colorchoices').should('be.visible')
.children('.agenda-table-colorchoice').should('have.length', 6)
.first().click({ scrollBehavior: false }).should('not.exist')
})
// Exit color assignment mode
cy.get('.agenda .agenda-table-colorpicker').click({ scrollBehavior: false })
// No colored dots should appear
cy.get('.agenda .agenda-table-display-event .agenda-table-colorindicator').should('not.exist')
// Clear all colors from Settings menu
cy.get('.meeting-nav').next('button').should('exist').and('be.visible').click()
cy.get('.agenda-settings').should('exist').and('be.visible')
cy.get('.agenda-settings .agenda-settings-actions > button').first().click()
cy.get('.n-dropdown-option:contains("Clear Color")').should('exist').and('be.visible').click()
// Color legend should no longer be displayed
cy.get('.agenda .agenda-colorlegend').should('not.exist')
cy.get('.agenda-settings').should('not.exist')
})
})
// ====================================================================
// AGENDA-NEUE (future meeting) | DESKTOP viewport
// ====================================================================
describe('meeting -> agenda-neue [future, desktop]', {
viewportWidth: viewports.desktop[0],
viewportHeight: viewports.desktop[1]
}, () => {
let meetingData = null
before(() => {
// Set clock to 2022-02-01 (month is 0-indexed)
cy.clock(new Date(2022, 1, 1))
// Generate future meeting data
meetingData = meetingGenerator.generateAgendaResponse({ dateMode: 'future' })
// Intercept Meeting Data API
cy.intercept('GET', `/api/meeting/${meetingData.meeting.number}/agenda-data`, { body: meetingData }).as('getMeetingData')
// Visit agenda page
cy.visit(`/meeting/${meetingData.meeting.number}/agenda-neue`, {
onBeforeLoad: (win) => { injectMeetingData(win, meetingData.meeting.number) }
})
cy.wait('@getMeetingData')
})
// -> SCHEDULE LIST -> Warning
it(`has current meeting warning`, () => {
cy.get('.agenda .agenda-currentwarn').should('exist').and('include.text', 'Note: IETF agendas are subject to change, up to and during a meeting.')
})
// -> SCHEDULE LIST -> Table Events
it('has schedule list table events (can take a while)', {
// This test is VERY memory-intensive, so disable DOM snapshots to prevent browser crash
numTestsKeptInMemory: 0
}, () => {
let isFirstSession = true
cy.get('tr.agenda-table-display-event').should('have.length', meetingData.schedule.length).each((el, idx) => {
// Apply small arbitrary wait every 10 rows to prevent the test UI from freezing
if (idx % 10 === 0) {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10)
}
const event = meetingData.schedule[idx]
// -----------------------
// Buttons / Status Column
// -----------------------
if (event.status === 'sched') {
if (event.flags.showAgenda || ['regular', 'plenary'].includes(event.type)) {
cy.wrap(el).find('.agenda-table-cell-links > .agenda-table-cell-links-buttons').as('eventbuttons')
if (event.flags.agenda) {
// Show meeting materials button
cy.get('@eventbuttons').find('i.bi.bi-collection').should('exist')
// ZIP materials button
cy.get('@eventbuttons').find(`#btn-lnk-${event.id}-tar`).should('have.attr', 'href', `/meeting/${meetingData.meeting.number}/agenda/${event.acronym}-drafts.tgz`)
.children('i.bi').should('exist')
// PDF materials button
cy.get('@eventbuttons').find(`#btn-lnk-${event.id}-pdf`).should('have.attr', 'href', `/meeting/${meetingData.meeting.number}/agenda/${event.acronym}-drafts.pdf`)
.children('i.bi').should('exist')
} else if (event.type === 'regular') {
// No meeting materials yet warning badge
cy.get('@eventbuttons').find('.no-meeting-materials').should('exist')
}
// Notepad button
const hedgeDocLink = `https://notes.ietf.org/notes-ietf-${meetingData.meeting.number}-${event.type === 'plenary' ? 'plenary' : event.acronym}`
cy.get('@eventbuttons').find(`#btn-lnk-${event.id}-note`).should('have.attr', 'href', hedgeDocLink)
.children('i.bi').should('exist')
// Chat room
cy.get('@eventbuttons').find(`#btn-lnk-${event.id}-room`).should('have.attr', 'href', event.links.chat)
.children('i.bi').should('exist')
// Video Stream
if (event.links.videoStream) {
cy.get('@eventbuttons').find(`#btn-lnk-${event.id}-video`).should('have.attr', 'href', formatLinkUrl(event.links.videoStream, event, meetingData.meeting.number))
.children('i.bi').should('exist')
}
// Onsite Tool
if (event.links.onsitetool) {
cy.get('@eventbuttons').find(`#btn-lnk-${event.id}-onsitetool`).should('have.attr', 'href', formatLinkUrl(event.links.onsitetool, event, meetingData.meeting.number))
.children('i.bi').should('exist')
}
// Audio Stream
if (event.links.audioStream) {
cy.get('@eventbuttons').find(`#btn-lnk-${event.id}-audio`).should('have.attr', 'href', formatLinkUrl(event.links.audioStream, event, meetingData.meeting.number))
.children('i.bi').should('exist')
}
// Remote Call-In
let remoteCallInUrl = null
if (event.note) {
remoteCallInUrl = findFirstConferenceUrl(event.note)
}
if (!remoteCallInUrl && event.remoteInstructions) {
remoteCallInUrl = findFirstConferenceUrl(event.remoteInstructions)
}
if (!remoteCallInUrl && event.links.webex) {
remoteCallInUrl = event.links.webex
}
if (remoteCallInUrl) {
cy.get('@eventbuttons').find(`#btn-lnk-${event.id}-remotecallin`).should('have.attr', 'href', remoteCallInUrl)
.children('i.bi').should('exist')
}
// calendar
if (event.links.calendar) {
cy.get('@eventbuttons').find(`#btn-lnk-${event.id}-calendar`).should('have.attr', 'href', event.links.calendar)
.children('i.bi').should('exist')
}
} else {
cy.wrap(el).find('.agenda-table-cell-links > .agenda-table-cell-links-buttons').should('not.exist')
}
}
})
})
})
// ====================================================================
// AGENDA-NEUE (live meeting) | DESKTOP viewport
// ====================================================================
describe('meeting -> agenda-neue [live, desktop]', {
viewportWidth: viewports.desktop[0],
viewportHeight: viewports.desktop[1]
}, () => {
let meetingData = null
const currentTime = DateTime.fromISO('2022-02-01T13:45:15', { zone: 'Asia/Tokyo' })
const liveEvents = []
let lastLiveEvent = null
before(() => {
// Set clock to 2022-02-01 (month is 0-indexed)
cy.clock(currentTime.toMillis())
// Generate live meeting data
meetingData = meetingGenerator.generateAgendaResponse({ dateMode: 'current' })
// Calculate live events
let lastEventStartTime = null
for (const event of meetingData.schedule) {
const eventStart = DateTime.fromISO(event.startDateTime, { zone: 'Asia/Tokyo' })
const eventEnd = eventStart.plus({ seconds: event.duration })
if (currentTime >= eventStart && currentTime < eventEnd) {
liveEvents.push(event)
// -> Find last event before current time
if (lastEventStartTime === eventStart.toMillis()) {
continue
} else {
lastEventStartTime = eventStart.toMillis()
lastLiveEvent = event
}
}
// -> Skip future events
if (eventStart > currentTime) {
break
}
}
// Intercept Meeting Data API
cy.intercept('GET', `/api/meeting/${meetingData.meeting.number}/agenda-data`, { body: meetingData }).as('getMeetingData')
// Visit agenda page
cy.visit(`/meeting/${meetingData.meeting.number}/agenda-neue`, {
onBeforeLoad: (win) => { injectMeetingData(win, meetingData.meeting.number) }
})
cy.wait('@getMeetingData')
// Fix scroll behavior
// See https://github.com/cypress-io/cypress/issues/3200
cy.document().then(document => {
const htmlElement = document.querySelector('html')
if (htmlElement) {
htmlElement.style.scrollBehavior = 'inherit'
}
})
})
beforeEach(() => {
cy.clock(currentTime.toMillis())
})
// -> HIGHLIGHTED LIVE SESSIONS
it(`has live sessions highlighted`, () => {
cy.get('.agenda .agenda-table-display-event.agenda-table-live').should('have.length', liveEvents.length)
})
// -> LIVE RED LINE
it(`has live red line`, () => {
cy.get('.agenda .agenda-table-redhand').should('be.visible').then(el => {
cy.get(`#agenda-rowid-${lastLiveEvent.id}`).then(elEv => {
expect(el.offsetTop).to.equal(elEv.offsetTop)
})
})
})
// -> JUMP TO NOW
it(`has jump to now button`, () => {
cy.get('.agenda .agenda-quickaccess-jumpto > .nav-item').should('have.length', 8).first().should('include.text', 'Now').click()
cy.get('.agenda .agenda-table-redhand').isInViewport()
})
// -> HIDE RED LINE
// TODO: dialog fails to render for unknown reason (but clicking manually on the window triggers the render)
// Seems like a cypress bug... Skipping for now.
it.skip(`can toggle the live red line`, () => {
// Open settings dialog
cy.get('.meeting-nav').next('button').click()
cy.get('.agenda-settings').should('exist').and('be.visible')
// Toggle red line switch
cy.get('.agenda-settings-content > .n-divider').eq(1).should('contain', 'Display')
.nextAll('div.d-flex').eq(5).find('div[role=switch]').as('switch-redline').click()
// Check red line disappeared
cy.get('.agenda .agenda-table-redhand').should('not.exist')
// Re-enable it
cy.get('@switch-redline').click()
// Check red line is visible again
cy.get('.agenda .agenda-table-redhand').should('be.visible')
// Close dialog
cy.get('.agenda-settings .agenda-settings-actions > button').last().click()
cy.get('.agenda-settings').should('not.exist')
})
})
// ====================================================================
// AGENDA-NEUE (past meeting) | SMALL DESKTOP/TABLET/MOBILE viewport
// ====================================================================
describe('meeting -> agenda-neue [past, small screens]', () => {
// Generate meeting data
const meetingData = meetingGenerator.generateAgendaResponse({ dateMode: 'past' })
for (const vp of ['smallDesktop', 'tablet', 'mobile']) {
describe(vp, {
viewportWidth: viewports[vp][0],
viewportHeight: viewports[vp][1]
}, () => {
before(() => {
// Set clock to 2022-02-01 (month is 0-indexed)
cy.clock(new Date(2022, 1, 1))
// Intercept Meeting Data API
cy.intercept('GET', `/api/meeting/${meetingData.meeting.number}/agenda-data`, { body: meetingData }).as('getMeetingData')
// Visit agenda page
cy.visit(`/meeting/${meetingData.meeting.number}/agenda-neue`, {
onBeforeLoad: (win) => { injectMeetingData(win, meetingData.meeting.number) }
})
cy.wait('@getMeetingData')
// Fix scroll behavior
// See https://github.com/cypress-io/cypress/issues/3200
cy.document().then(document => {
const htmlElement = document.querySelector('html')
if (htmlElement) {
htmlElement.style.scrollBehavior = 'inherit'
}
})
})
// -> NARROW QUICK ACCESS PANEL (smallDesktop only)
if (vp === 'smallDesktop') {
it('has narrow quick access panel', () => {
// Alternate labels for buttons
cy.get('#agenda-quickaccess-filterbyareagroups-btn').should('be.visible').and('include.text', 'Filter...')
.next('button').should('be.visible').and('include.text', 'Pick...')
cy.get('#agenda-quickaccess-calview-btn').should('be.visible').and('include.text', 'Cal View')
.next('button').should('be.visible').and('include.text', '.ics')
// -> Shorter date labels for Jump to buttons
cy.get('.agenda .agenda-quickaccess-jumpto > .nav-item').should('have.length', 7).as('dayjumpbuttons')
.each((el, idx) => {
const localDateTime = DateTime.fromISO(meetingData.meeting.startDate).setZone('local').plus({ days: idx }).toFormat('ccc LLL d')
cy.wrap(el).should('contain', localDateTime).find('i.bi').should('not.be.visible')
})
// Take a snapshot for visual diffing
cy.percySnapshot(`meeting -> agenda-neue [past, ${vp}]`, { widths: [viewports[vp][0]] })
})
}
// -> TABLET + MOBILE-specific tests
if (vp === 'tablet' || vp === 'mobile') {
// Check for elements that should not exist on smaller screens
it('has no updated date', () => {
cy.get('.agenda > h4 > h6').should('not.be.visible')
// Take a snapshot for visual diffing
cy.percySnapshot(`meeting -> agenda-neue [past, ${vp}]`, { widths: [viewports[vp][0]] })
})
it('has no timezone dropdown selector', () => {
cy.get('.agenda .agenda-tz-selector').next('.agenda-timezone-ddn').should('not.exist')
})
it('has no floor + group indicators', () => {
cy.get('.agenda .agenda-table-cell-room > .badge').should('not.be.visible')
cy.get('.agenda .agenda-table-cell-group > .badge').should('not.exist')
})
// Session buttons should be hidden in a dropdown menu
it('has session buttons dropdown', () => {
cy.get('.agenda .agenda-table-display-event .agenda-table-cell-links-buttons').each(el => {
cy.wrap(el).children().should('have.length', 1)
})
// TODO: Check for dropdown links once changed to a custom panel with standard links
})
// Bottom Mobile Bar
it('has no lateral quick access panel', () => {
cy.get('.agenda-quickaccess').should('not.exist')
})
it('has a bottom mobile bar', () => {
cy.get('.agenda-mobile-bar').should('be.visible')
.children().should('have.length', 4)
.first().should('include.text', 'Filters')
.next().should('include.text', 'Cal')
.next().should('include.text', '.ics')
.next().children().should('have.length', 1).and('have.class', 'bi')
})
it('can open the filters overlay', () => {
cy.get('.agenda-mobile-bar > button').first().click()
cy.get('.agenda-personalize').should('be.visible')
cy.get('.agenda-personalize .agenda-personalize-actions > button').eq(1).click()
cy.get('.agenda-personalize').should('not.exist')
})
it('can open the calendar view', () => {
cy.get('.agenda-mobile-bar > button').eq(1).click()
cy.get('.agenda-calendar').should('be.visible')
cy.get('.agenda-calendar .agenda-calendar-actions > button').eq(1).click()
cy.get('.agenda-calendar').should('not.exist')
})
it('can open the ics dropdown', () => {
cy.get('.agenda-mobile-bar > button').eq(2).click()
cy.get('.n-dropdown-menu > .n-dropdown-option').should('have.length', 2)
.first().should('include.text', 'Subscribe')
.next().should('include.text', 'Download')
})
it('can open the settings overlay', () => {
cy.get('.agenda-mobile-bar > button').last().click()
cy.get('.agenda-settings').should('be.visible')
cy.get('.agenda-settings .agenda-settings-actions > button').eq(1).click()
cy.get('.agenda-settings').should('not.exist')
})
}
})
}
})
// ====================================================================
// FLOOR-PLAN-NEUE | All Viewports
// ====================================================================
describe(`meeting -> floor-plan-neue`, () => {
for (const vp of ['desktop', 'smallDesktop', 'tablet', 'mobile']) {
describe(vp, {
viewportWidth: viewports[vp][0],
viewportHeight: viewports[vp][1]
}, () => {
const meetingData = meetingGenerator.generateAgendaResponse({ dateMode: 'past', skipSchedule: true })
before(() => {
cy.intercept('GET', `/api/meeting/${meetingData.meeting.number}/agenda-data`, { body: meetingData }).as('getMeetingData')
cy.visit(`/meeting/${meetingData.meeting.number}/floor-plan-neue`, {
onBeforeLoad: (win) => { injectMeetingData(win, meetingData.meeting.number) }
})
cy.wait('@getMeetingData')
})
// -> HEADER
it(`has IETF ${meetingData.meeting.number} title`, () => {
cy.get('.floorplan h1').first().contains(`IETF ${meetingData.meeting.number} Floor Plan`)
// Take a snapshot for visual diffing
cy.percySnapshot(`meeting -> floor-plan-neue [${vp}]`, { widths: [viewports[vp][0]] })
})
it(`has meeting city subtitle`, () => {
cy.get('.floorplan h4').first().contains(meetingData.meeting.city)
})
it(`has meeting date subtitle`, () => {
cy.get('.floorplan h4').first().contains(/[a-zA-Z] [0-9]{1,2} - ([a-zA-Z]+ )?[0-9]{1,2}, [0-9]{4}/i)
})
// -> NAV
it(`has the correct navigation items`, () => {
cy.get('.floorplan .meeting-nav > li').should('have.length', 3)
cy.get('.floorplan .meeting-nav > li').first().contains('Agenda')
cy.get('.floorplan .meeting-nav > li').eq(1).contains('Floor plan')
cy.get('.floorplan .meeting-nav > li').last().contains('Plaintext')
})
// -> FLOORS
it(`can switch between floors`, () => {
cy.get('.floorplan .floorplan-floors > .nav-link').should('have.length', meetingData.floors.length)
cy.get('.floorplan .floorplan-floors > .nav-link').each((el, idx) => {
cy.wrap(el).contains(meetingData.floors[idx].name)
cy.wrap(el).click()
cy.wrap(el).should('have.class', 'active')
cy.wrap(el).siblings().should('not.have.class', 'active')
// Wait for image to load + verify
cy.get('.floorplan .floorplan-plan > img').should('be.visible').and(img => expect(img[0].naturalWidth).to.be.greaterThan(1))
})
})
// -> ROOMS
it(`can select rooms`, { retries: 2 }, () => {
const floor = meetingData.floors[0]
cy.get('.floorplan .floorplan-floors > .nav-link').first().click()
cy.get('.floorplan .floorplan-rooms > .list-group-item').should('have.length', floor.rooms.length)
cy.get('.floorplan .floorplan-rooms > .list-group-item').each((el, idx) => {
// Room List
const room = floor.rooms[idx]
cy.wrap(el).find('strong').contains(room.name)
.next('small').contains(room.functionalName)
cy.wrap(el).find('.badge').should('exist').and('include.text', floor.short)
cy.wrap(el).click()
cy.wrap(el).should('have.class', 'active')
cy.wrap(el).siblings().should('not.have.class', 'active')
// URL query segment
cy.location('search').should('include', `room=${room.slug}`)
// Pin Drop
cy.window().then(win => {
cy.get('.floorplan .floorplan-plan > img').then(floorImg => {
const planxRatio = floorImg[0].width / floor.width
const planyRatio = floorImg[0].height / floor.height
cy.get('.floorplan .floorplan-plan-pin').should('exist').then(el => {
const pinMarginLeft = parseInt(win.getComputedStyle(el[0]).getPropertyValue('margin-left').match(/\d+/))
const xPos = Math.round((room.left + (room.right - room.left) / 2) * planxRatio) - 25 + pinMarginLeft
const yPos = Math.round((room.top + (room.bottom - room.top) / 2) * planyRatio) - 40
expect(el[0].offsetLeft).to.equal(xPos)
expect(el[0].offsetTop).to.equal(yPos)
})
})
})
})
})
})
}
})