datatracker/playwright/tests/meeting/agenda.spec.js
Nicolas Giard 4a1d29e86c
feat: add wiki button to agenda list for hackathon sessions (#8133)
* feat: add wiki button to agenda list for hackathon sessions

* fix: update client/agenda/AgendaScheduleList.vue

Co-authored-by: Matthew Holloway <matthew@holloway.co.nz>

* fix: broken tests

---------

Co-authored-by: Matthew Holloway <matthew@holloway.co.nz>
Co-authored-by: Robert Sparks <rjsparks@nostrum.com>
2024-12-16 08:51:33 -06:00

1486 lines
72 KiB
JavaScript

const { test, expect } = require('@playwright/test')
const { DateTime } = require('luxon')
const { faker } = require('@faker-js/faker')
const seedrandom = require('seedrandom')
const slugify = require('slugify')
const commonHelper = require('../../helpers/common')
const meetingHelper = require('../../helpers/meeting.js')
const viewports = require('../../helpers/viewports')
const _ = require('lodash')
const fs = require('fs/promises')
const { setTimeout } = require('timers/promises')
const xslugify = (str) => slugify(str.replace('/', '-'), { lower: true, strict: true })
const TEST_SEED = 123
const BROWSER_LOCALE = 'en-US'
const BROWSER_TIMEZONE = 'America/Toronto'
// Set randomness seed
seedrandom(TEST_SEED.toString(), { global: true })
faker.seed(TEST_SEED)
const { random, shuffle } = _.runInContext()
// ====================================================================
// AGENDA (past meeting) | DESKTOP viewport
// ====================================================================
test.describe('past - desktop', () => {
let meetingData
test.beforeAll(async () => {
// Generate meeting data
meetingData = meetingHelper.generateAgendaResponse({ dateMode: 'past' })
})
test.beforeEach(async ({ page }) => {
// Intercept Meeting Data API
await page.route(`**/api/meeting/${meetingData.meeting.number}/agenda-data`, route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(meetingData)
})
})
await page.setViewportSize({
width: viewports.desktop[0],
height: viewports.desktop[1]
})
// Visit agenda page and await Meeting Data API call to complete
await Promise.all([
page.waitForResponse(`**/api/meeting/${meetingData.meeting.number}/agenda-data`),
page.goto(`/meeting/${meetingData.meeting.number}/agenda`)
])
// Wait for page to be ready
await page.locator('.agenda h1').waitFor({ state: 'visible' })
await setTimeout(500)
})
test('agenda header section', async ({ page }) => {
// HEADER
await expect(page.locator('.agenda h1'), 'should have agenda title').toContainText(`IETF ${meetingData.meeting.number} Meeting Agenda`)
await expect(page.locator('.agenda h4').first(), 'should have meeting city subtitle').toContainText(meetingData.meeting.city)
await expect(page.locator('.agenda h4').first(), 'should have meeting date subtitle').toContainText(/[a-zA-Z] [0-9]{1,2} - ([a-zA-Z]+ )?[0-9]{1,2}, [0-9]{4}/i)
const updatedDateTime = DateTime.fromISO(meetingData.meeting.updated)
.setZone(meetingData.meeting.timezone)
.setLocale(BROWSER_LOCALE)
.toFormat('DD \'at\' T ZZZZ')
await expect(page.locator('.agenda h6').first(), 'should have meeting last updated datetime').toContainText(updatedDateTime)
// NAV
await test.step('has the correct navigation items', async () => {
const navLocator = page.locator('.agenda .meeting-nav > li')
await expect(navLocator).toHaveCount(3)
await expect(navLocator.first()).toContainText('Agenda')
await expect(navLocator.nth(1)).toContainText('Floor plan')
await expect(navLocator.last()).toContainText('Plaintext')
})
// RIGHT-SIDE BUTTONS
await test.step('has the correct right side buttons', async () => {
const btnsLocator = page.locator('.agenda .agenda-topnav-right > button')
await expect(btnsLocator).toHaveCount(3)
await expect(btnsLocator.first()).toContainText('Help')
await expect(btnsLocator.nth(1)).toContainText('Share')
await expect(btnsLocator.last()).toContainText('Settings')
})
})
test('agenda schedule list header', async ({ page }) => {
const infonoteLocator = page.locator('.agenda .agenda-infonote')
const infonoteToggleLocator = page.locator('.agenda h2 + button')
const tzMeetingBtnLocator = page.locator('.agenda .agenda-tz-selector > button:nth-child(1)')
const tzLocalBtnLocator = page.locator('.agenda .agenda-tz-selector > button:nth-child(2)')
const tzUtcBtnLocator = page.locator('.agenda .agenda-tz-selector > button:nth-child(3)')
await expect(page.locator('.agenda h2')).toContainText('Schedule')
await expect(infonoteLocator).toBeVisible()
await expect(infonoteLocator).toContainText(meetingData.meeting.infoNote)
// INFO-NOTE TOGGLE
await test.step('info note can be dismissed / reopened', async () => {
await page.locator('.agenda .agenda-infonote > button').click()
await expect(infonoteLocator).not.toBeVisible()
await expect(infonoteToggleLocator).toBeVisible()
await infonoteToggleLocator.click()
await expect(infonoteLocator).toBeVisible()
await expect(infonoteToggleLocator).not.toBeVisible()
})
// TIMEZONE SELECTOR
await test.step('has timezone selector', async () => {
await expect(page.locator('.agenda .agenda-tz-selector')).toBeVisible()
await expect(page.locator('small:left-of(.agenda .agenda-tz-selector)')).toContainText('Timezone:')
await expect(page.locator('.agenda .agenda-tz-selector > button')).toHaveCount(3)
await expect(tzMeetingBtnLocator).toContainText('Meeting')
await expect(tzLocalBtnLocator).toContainText('Local')
await expect(tzUtcBtnLocator).toContainText('UTC')
await expect(page.locator('.agenda .agenda-timezone-ddn')).toBeVisible()
})
// CHANGE TIMEZONE
await test.step('can change timezone', async () => {
// Switch to local timezone
await tzLocalBtnLocator.click()
await expect(tzLocalBtnLocator).toHaveClass(/n-button--primary-type/)
await expect(tzMeetingBtnLocator).not.toHaveClass(/n-button--primary-type/)
const localDateTime = DateTime.fromISO(meetingData.meeting.updated)
.setZone(BROWSER_TIMEZONE)
.setLocale(BROWSER_LOCALE)
.toFormat('DD \'at\' T ZZZZ')
await expect(page.locator('.agenda h6').first()).toContainText(localDateTime)
await expect(page.locator('.agenda .agenda-table-display-session-head .agenda-table-cell-name').first()).toContainText('Monday Session I')
// Switch to UTC
await tzUtcBtnLocator.click()
await expect(tzUtcBtnLocator).toHaveClass(/n-button--primary-type/)
await expect(tzLocalBtnLocator).not.toHaveClass(/n-button--primary-type/)
const utcDateTime = DateTime.fromISO(meetingData.meeting.updated)
.setZone('utc')
.setLocale(BROWSER_LOCALE)
.toFormat('DD \'at\' T ZZZZ')
await expect(page.locator('.agenda h6').first()).toContainText(utcDateTime)
await expect(page.locator('.agenda .agenda-timezone-ddn')).toContainText('UTC')
await expect(page.locator('.agenda .agenda-table-display-session-head .agenda-table-cell-name').first()).toContainText('Monday Session I')
// Switch back to meeting timezone
await tzMeetingBtnLocator.click()
await expect(tzMeetingBtnLocator).toHaveClass(/n-button--primary-type/)
await expect(page.locator('.agenda .agenda-timezone-ddn')).toContainText('Tokyo')
await expect(page.locator('.agenda .agenda-table-display-session-head .agenda-table-cell-name').first()).toContainText('Monday Session I')
})
})
test('agenda schedule list table', async ({ page }) => {
const dayHeadersLocator = page.locator('.agenda-table-display-day')
// TABLE HEADERS
await expect(page.locator('.agenda-table-head-time')).toContainText('Time')
await expect(page.locator('.agenda-table-head-location')).toContainText('Location')
await expect(page.locator('.agenda-table-head-event')).toContainText('Event')
// DAY HEADERS
await expect(dayHeadersLocator).toHaveCount(7)
for (let idx = 0; idx < 7; idx++) {
const localDateTime = DateTime.fromISO(meetingData.meeting.startDate, { zone: BROWSER_TIMEZONE })
.setZone(BROWSER_TIMEZONE)
.setLocale(BROWSER_LOCALE)
.plus({ days: idx })
.toLocaleString(DateTime.DATE_HUGE)
await expect(dayHeadersLocator.nth(idx)).toContainText(localDateTime)
}
})
test('agenda schedule list table events', async ({ page }) => {
test.slow() // Triple the default timeout
const eventRowsLocator = page.locator('.agenda-table .agenda-table-display-event')
await expect(eventRowsLocator).toHaveCount(meetingData.schedule.length)
let isFirstSession = true
for (let idx = 0; idx < meetingData.schedule.length; idx++) {
const row = eventRowsLocator.nth(idx)
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
await expect(row.locator('.agenda-table-cell-room > a')).toContainText(event.room)
await expect(row.locator('.agenda-table-cell-room > a')).toHaveAttribute('href', `/meeting/${meetingData.meeting.number}/floor-plan?room=${xslugify(event.room)}`)
await expect(row.locator('.agenda-table-cell-room > .badge')).toContainText(event.location.short)
} else {
// No floor badge
await expect(row.locator('.agenda-table-cell-room > span:not(.badge)')).toContainText(event.room)
await expect(row.locator('.agenda-table-cell-room > .badge')).not.toBeVisible()
}
// ---------------------------------------------------
// Type-specific timeslot / group / name columns tests
// ---------------------------------------------------
if (event.type === 'regular') {
// First session should have header row above it
if (isFirstSession) {
const headerRow = page.locator(`#agenda-rowid-sesshd-${event.id}`)
await expect(headerRow).toBeVisible()
await expect(headerRow.locator('.agenda-table-cell-ts')).toContainText(eventTimeSlot)
await expect(headerRow.locator('.agenda-table-cell-name')).toContainText(`${DateTime.fromISO(event.startDateTime).toFormat('cccc')} ${event.slotName}`)
}
// Timeslot
await expect(row.locator('.agenda-table-cell-ts')).toContainText('—')
// Group Acronym + Parent
await expect(row.locator('.agenda-table-cell-group > .badge')).toContainText(event.groupParent.acronym)
await expect(row.locator('.agenda-table-cell-group > .badge + a')).toContainText(event.acronym)
await expect(row.locator('.agenda-table-cell-group > .badge + a')).toHaveAttribute('href', `/group/${event.acronym}/about/`)
// Group Name
await expect(row.locator('.agenda-table-cell-name')).toContainText(event.groupName)
isFirstSession = false
} else {
// Timeslot
await expect(row.locator('.agenda-table-cell-ts')).toContainText(eventTimeSlot)
// Event Name
await expect(row.locator('.agenda-table-cell-name')).toContainText(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))) {
await expect(row.locator('.agenda-table-cell-name > i.bi')).toBeVisible()
}
// Name link
if (event.flags.agenda) {
await expect(row.locator('.agenda-table-cell-name > a')).toHaveAttribute('href', event.agenda.url)
}
// BoF badge
if (event.isBoF) {
await expect(row.locator('.agenda-table-cell-name > .badge')).toContainText('BoF')
}
// Note
if (event.note) {
await expect(row.locator('.agenda-table-cell-name > .agenda-table-note')).toBeVisible()
await expect(row.locator('.agenda-table-cell-name > .agenda-table-note i.bi')).toBeVisible()
await expect(row.locator('.agenda-table-cell-name > .agenda-table-note i.bi + span')).toContainText(event.note)
}
// -----------------------
// Buttons / Status Column
// -----------------------
switch (event.status) {
// Cancelled
case 'canceled': {
await expect(row.locator('.agenda-table-cell-links > .badge.is-cancelled')).toContainText('Cancelled')
break
}
// Rescheduled
case 'resched': {
await expect(row.locator('.agenda-table-cell-links > .badge.is-rescheduled')).toContainText('Rescheduled')
break
}
// Scheduled
case 'sched': {
if (event.flags.showAgenda || (['regular', 'plenary', 'other'].includes(event.type) && !['admin', 'closed_meeting', 'officehours', 'social'].includes(event.purpose))) {
const eventButtons = row.locator('.agenda-table-cell-links > .agenda-table-cell-links-buttons')
if (event.flags.agenda) {
// Show meeting materials button
await expect(eventButtons.locator('i.bi.bi-collection')).toBeVisible()
// ZIP materials button
await expect(eventButtons.locator(`#btn-lnk-${event.id}-tar`)).toHaveAttribute('href', `/meeting/${meetingData.meeting.number}/agenda/${event.acronym}-drafts.tgz`)
await expect(eventButtons.locator(`#btn-lnk-${event.id}-tar > i.bi`)).toBeVisible()
// PDF materials button
await expect(eventButtons.locator(`#btn-lnk-${event.id}-pdf`)).toHaveAttribute('href', `/meeting/${meetingData.meeting.number}/agenda/${event.acronym}-drafts.pdf`)
await expect(eventButtons.locator(`#btn-lnk-${event.id}-pdf > i.bi`)).toBeVisible()
} else if (event.type === 'regular') {
// No meeting materials yet warning badge
await expect(eventButtons.locator('.no-meeting-materials')).toBeVisible()
}
if (event.name.toLowerCase().includes('hackathon')) {
// Hackathon Wiki button
const hackathonWikiLink = `https://wiki.ietf.org/meeting/${meetingData.meeting.number}/hackathon`
await expect(eventButtons.locator(`#btn-lnk-${event.id}-wiki`)).toHaveAttribute('href', hackathonWikiLink)
await expect(eventButtons.locator(`#btn-lnk-${event.id}-wiki > i.bi`)).toBeVisible()
} else {
// Notepad button
const hedgeDocLink = `https://notes.ietf.org/notes-ietf-${meetingData.meeting.number}-${event.type === 'plenary' ? 'plenary' : event.acronym}`
await expect(eventButtons.locator(`#btn-lnk-${event.id}-note`)).toHaveAttribute('href', hedgeDocLink)
await expect(eventButtons.locator(`#btn-lnk-${event.id}-note > i.bi`)).toBeVisible()
}
// Chat logs
await expect(eventButtons.locator(`#btn-lnk-${event.id}-logs`)).toHaveAttribute('href', event.links.chatArchive)
await expect(eventButtons.locator(`#btn-lnk-${event.id}-logs > i.bi`)).toBeVisible()
// Recordings
for (const rec of event.links.recordings) {
if (rec.url.indexOf('audio') > 0) {
// -> Audio
await expect(eventButtons.locator(`#btn-lnk-${event.id}-audio-${rec.id}`)).toHaveAttribute('href', rec.url)
await expect(eventButtons.locator(`#btn-lnk-${event.id}-audio-${rec.id} > i.bi`)).toBeVisible()
} else if (rec.url.indexOf('youtu') > 0) {
// -> Youtube
await expect(eventButtons.locator(`#btn-lnk-${event.id}-youtube-${rec.id}`)).toHaveAttribute('href', rec.url)
await expect(eventButtons.locator(`#btn-lnk-${event.id}-youtube-${rec.id} > i.bi`)).toBeVisible()
} else {
// -> Others
await expect(eventButtons.locator(`#btn-lnk-${event.id}-video-${rec.id}`)).toHaveAttribute('href', rec.url)
await expect(eventButtons.locator(`#btn-lnk-${event.id}-video-${rec.id} > i.bi`)).toBeVisible()
}
}
// Video Stream
if (event.links.videoStream) {
const videoStreamLink = `https://www.meetecho.com/ietf${meetingData.meeting.number}/recordings#${event.acronym.toUpperCase()}`
await expect(eventButtons.locator(`#btn-lnk-${event.id}-rec`)).toHaveAttribute('href', videoStreamLink)
await expect(eventButtons.locator(`#btn-lnk-${event.id}-rec > i.bi`)).toBeVisible()
}
} else {
await expect(row.locator('.agenda-table-cell-links > .agenda-table-cell-links-buttons')).not.toBeVisible()
}
break
}
}
}
})
test('agenda schedule list search', async ({ page }) => {
const eventRowsLocator = page.locator('.agenda-table .agenda-table-display-event')
const searchInputLocator = page.locator('.agenda-search input[type=text]')
await page.locator('.agenda-table > .agenda-table-search > button').click()
await expect(page.locator('.agenda-search')).toBeVisible()
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) {
await searchInputLocator.fill(term)
// Let the UI update before checking each displayed row
await page.waitForTimeout(1000)
await expect(eventRowsLocator).not.toHaveCount(meetingData.schedule.length)
const rowsCount = await eventRowsLocator.count()
for (let idx = 0; idx < rowsCount; idx++) {
await expect(eventRowsLocator.nth(idx)).toContainText(term, { ignoreCase: true })
}
}
// Clear button
await page.locator('.agenda-search button').click()
await page.waitForTimeout(1000)
await expect(searchInputLocator).toHaveValue('')
await expect(eventRowsLocator).toHaveCount(meetingData.schedule.length)
// Invalid search
await searchInputLocator.fill(faker.vehicle.vin())
await page.waitForTimeout(1000)
await expect(eventRowsLocator).toHaveCount(0)
await expect(page.locator('.agenda-table .agenda-table-display-noresult')).toContainText('No event matching your search query.')
// Closing search should clear search
await page.locator('.agenda-table > .agenda-table-search > button').click()
await expect(page.locator('.agenda-search')).not.toBeVisible()
await expect(eventRowsLocator).toHaveCount(meetingData.schedule.length)
})
test('agenda meeting materials dialog', async ({ page }) => {
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: {
decks: _.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]
})),
actions: [{
label: 'Propose slides',
url: `/meeting/${meetingData.meeting.number}/session/${event.sessionId}/propose_slides`
}]
},
minutes: {
ext: 'md',
id: 123456,
title: 'Minutes IETF123 Testing',
url: `/meeting/${meetingData.meeting.number}/materials/minutes-${meetingData.meeting.number}-${event.acronym}-${faker.internet.domainWord()}`
}
}
await page.route(`**/api/meeting/session/${event.sessionId}/materials`, route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(materialsInfo)
})
})
await page.route(materialsUrl, route => {
route.fulfill({
status: 200,
contentType: 'text/plain',
body: 'The internet is a series of tubes.'
})
})
await page.route(materialsInfo.minutes.url, route => {
route.fulfill({
status: 200,
contentType: 'text/plain',
body: 'One does not simply walk into mordor.'
})
})
// Open dialog
await page.locator(`#agenda-rowid-${event.id} #btn-lnk-${event.id}-mat`).click()
await expect(page.locator('.agenda-eventdetails')).toBeVisible()
// Header
await expect(page.locator('.agenda-eventdetails .n-card-header__main > .detail-header > .bi')).toBeVisible()
await expect(page.locator('.agenda-eventdetails .n-card-header__main > .detail-header > .bi + span')).toContainText(eventStart.toFormat('DDDD'))
await expect(page.locator('.agenda-eventdetails .n-card-header__extra > .detail-header > .bi')).toBeVisible()
await expect(page.locator('.agenda-eventdetails .n-card-header__extra > .detail-header > .bi + strong')).toContainText(`${eventStart.toFormat('T')} - ${eventEnd.toFormat('T')}`)
await expect(page.locator('.agenda-eventdetails .detail-title > h6 > .bi')).toBeVisible()
await expect(page.locator('.agenda-eventdetails .detail-title > h6 > .bi + span')).toContainText(event.name)
await expect(page.locator('.agenda-eventdetails .detail-location > .bi')).toBeVisible()
await expect(page.locator('.agenda-eventdetails .detail-location > .bi + .badge')).toContainText(event.location.short)
await expect(page.locator('.agenda-eventdetails .detail-location > .bi + .badge + span')).toContainText(event.room)
// Navigation
const navLocator = await page.locator('.agenda-eventdetails .detail-nav > a')
await expect(navLocator).toHaveCount(3)
await expect(navLocator.first()).toHaveClass(/active/)
await expect(navLocator.nth(1)).not.toHaveClass(/active/)
await expect(navLocator.nth(2)).not.toHaveClass(/active/)
// Agenda Tab
await expect(page.locator('.agenda-eventdetails .detail-text > iframe')).toHaveAttribute('src', materialsUrl)
// Slides Tab
await navLocator.nth(1).click()
await expect(navLocator.nth(1)).toHaveClass(/active/)
await expect(navLocator.first()).not.toHaveClass(/active/)
const slideDecksLocator = page.locator('.agenda-eventdetails .detail-text .n-card__content > .list-group > .list-group-item')
await expect(slideDecksLocator).toHaveCount(materialsInfo.slides.decks.length)
for (let idx = 0; idx < materialsInfo.slides.decks.length; idx++) {
await expect(slideDecksLocator.nth(idx)).toHaveAttribute('href', materialsInfo.slides.decks[idx].url)
await expect(slideDecksLocator.nth(idx).locator('.bi')).toHaveClass(new RegExp(`bi-filetype-${materialsInfo.slides.decks[idx].ext}`))
await expect(slideDecksLocator.nth(idx).locator('span')).toContainText(materialsInfo.slides.decks[idx].title)
}
const slideActionButtonLocator = page.locator('.agenda-eventdetails .detail-text .n-card__action > a')
await expect(slideActionButtonLocator).toHaveCount(1)
await expect(slideActionButtonLocator.first().locator('span')).toContainText('Propose slides')
// Minutes Tab
await navLocator.last().click()
await expect(navLocator.last()).toHaveClass(/active/)
await expect(navLocator.nth(1)).not.toHaveClass(/active/)
await expect(page.locator('.agenda-eventdetails .detail-text > iframe')).toHaveAttribute('src', materialsInfo.minutes.url)
// Footer Buttons
const hedgeDocLink = `https://notes.ietf.org/notes-ietf-${meetingData.meeting.number}-${event.type === 'plenary' ? 'plenary' : event.acronym}`
const detailsUrl = `/meeting/${meetingData.meeting.number}/session/${event.acronym}/`
const footerBtnsLocator = page.locator('.agenda-eventdetails .detail-action > a')
await expect(footerBtnsLocator).toHaveCount(4)
await expect(footerBtnsLocator.first()).toContainText('Download as tarball')
await expect(footerBtnsLocator.first()).toHaveAttribute('href', `/meeting/${meetingData.meeting.number}/agenda/${event.acronym}-drafts.tgz`)
await expect(footerBtnsLocator.nth(1)).toContainText('Download as PDF')
await expect(footerBtnsLocator.nth(1)).toHaveAttribute('href', `/meeting/${meetingData.meeting.number}/agenda/${event.acronym}-drafts.pdf`)
await expect(footerBtnsLocator.nth(2)).toContainText('Notepad')
await expect(footerBtnsLocator.nth(2)).toHaveAttribute('href', hedgeDocLink)
await expect(footerBtnsLocator.last()).toContainText(`${event.groupAcronym} materials page`)
await expect(footerBtnsLocator.last()).toHaveAttribute('href', detailsUrl)
// Clicking X should close the dialog
await page.locator('.agenda-eventdetails .n-card-header__extra > .detail-header > button').click()
})
// -> SCHEDULE LIST -> Show Meeting Materials dialog (EMPTY VARIANT)
test('agenda meeting materials dialog (empty variant)', async ({ page }) => {
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
}
await page.route(`**/api/meeting/session/${event.sessionId}/materials`, route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(materialsInfo)
})
})
await page.route(materialsUrl, route => {
route.fulfill({
status: 200,
contentType: 'text/plain',
body: 'The internet is a series of tubes.'
})
})
// Open dialog
await page.locator(`#btn-lnk-${event.id}-mat`).click()
await expect(page.locator('.agenda-eventdetails')).toBeVisible()
// Slides Tab
await page.locator('.agenda-eventdetails .detail-nav > a').nth(1).click()
await expect(page.locator('.agenda-eventdetails .detail-text .n-card__content')).toContainText('No slides submitted for this session.')
// Minutes Tab
await page.locator('.agenda-eventdetails .detail-nav > a').nth(2).click()
await expect(page.locator('.agenda-eventdetails .detail-text')).toContainText('No minutes submitted for this session.')
// Clicking X should close the dialog
await page.locator('.agenda-eventdetails .n-card-header__extra > .detail-header > button').click()
})
// -> FILTER BY AREA/GROUP DIALOG
test('agenda filter by area/group', async ({ page }) => {
test.slow() // Triple the default timeout
// Open dialog
await page.locator('#agenda-quickaccess-filterbyareagroups-btn').click()
await expect(page.locator('.agenda-personalize')).toBeVisible()
// Check header elements
await expect(page.locator('.agenda-personalize .n-drawer-header__main > span')).toContainText('Filter Areas + Groups')
const diagHeaderBtnLocator = page.locator('.agenda-personalize .agenda-personalize-actions > button')
await expect(diagHeaderBtnLocator).toHaveCount(3)
await expect(diagHeaderBtnLocator.first()).toContainText('Clear Selection')
await expect(diagHeaderBtnLocator.nth(1)).toContainText('Cancel')
await expect(diagHeaderBtnLocator.last()).toContainText('Apply')
// Check categories
const catsLocator = page.locator('.agenda-personalize .agenda-personalize-category')
await expect(catsLocator).toHaveCount(meetingData.categories.length)
// Check areas + groups
for (let idx = 0; idx < meetingData.categories.length; idx++) {
const cat = meetingData.categories[idx]
const areasLocator = catsLocator.nth(idx).locator('.agenda-personalize-area')
await expect(areasLocator).toHaveCount(cat.length)
for (let areaIdx = 0; areaIdx < cat.length; areaIdx++) {
// Area Button
const area = cat[areaIdx]
if (area.label) {
await expect(areasLocator.nth(areaIdx).locator('.agenda-personalize-areamain > button')).toBeVisible()
await expect(areasLocator.nth(areaIdx).locator('.agenda-personalize-areamain > button')).toContainText(area.label)
} else {
await expect(areasLocator.nth(areaIdx).locator('.agenda-personalize-areamain > button')).not.toBeVisible()
}
// Group Buttons
const grpBtnsLocator = areasLocator.nth(areaIdx).locator('.agenda-personalize-groups > button')
await expect(grpBtnsLocator).toHaveCount(area.children.length)
for (let groupIdx = 0; groupIdx < area.children.length; groupIdx++) {
const group = area.children[groupIdx]
await expect(grpBtnsLocator.nth(groupIdx)).toBeVisible()
await expect(grpBtnsLocator.nth(groupIdx)).toContainText(group.label)
if (group.is_bof) {
await expect(grpBtnsLocator.nth(groupIdx)).toHaveClass(/is-bof/)
await expect(grpBtnsLocator.nth(groupIdx).locator('.badge')).toBeVisible()
await expect(grpBtnsLocator.nth(groupIdx).locator('.badge')).toContainText('BoF')
}
}
// Test Area Selection
if (area.label) {
await areasLocator.nth(areaIdx).locator('.agenda-personalize-areamain > button').click()
for (let groupIdx = 0; groupIdx < area.children.length; groupIdx++) {
await expect(grpBtnsLocator.nth(groupIdx)).toHaveClass(/is-checked/)
}
await areasLocator.nth(areaIdx).locator('.agenda-personalize-areamain > button').click()
for (let groupIdx = 0; groupIdx < area.children.length; groupIdx++) {
await expect(grpBtnsLocator.nth(groupIdx)).not.toHaveClass(/is-checked/)
}
}
// Test Group Selection
const randGroupIdx = random(area.children.length - 1)
const groupLocator = areasLocator.nth(areaIdx).locator('.agenda-personalize-groups > button').nth(randGroupIdx)
await groupLocator.click()
await expect(groupLocator).toHaveClass(/is-checked/)
await groupLocator.click()
await expect(groupLocator).not.toHaveClass(/is-checked/)
}
}
// Test multi-toggled_by button trigger
const bofBtnLocator = page.locator('.agenda-personalize .agenda-personalize-category:last-child .agenda-personalize-area:last-child .agenda-personalize-groups > button', { hasText: 'BoF' })
const bofGroupsLocator = page.locator('.agenda-personalize .agenda-personalize-group:has(.badge)')
const bofGroupsCount = await bofGroupsLocator.count()
await bofBtnLocator.click()
for (let idx = 0; idx < bofGroupsCount; idx++) {
await expect(bofGroupsLocator.nth(idx)).toHaveClass(/is-checked/)
}
await bofBtnLocator.click()
for (let idx = 0; idx < bofGroupsCount; idx++) {
await expect(bofGroupsLocator.nth(idx)).not.toHaveClass(/is-checked/)
}
// Clicking all groups from area then area button should unselect all
const areaGroupsLocator = page.locator('.agenda-personalize .agenda-personalize-area >> nth=0 >> .agenda-personalize-groups > button')
const areaGroupsCount = await areaGroupsLocator.count()
for (let idx = 0; idx < areaGroupsCount; idx++) {
await areaGroupsLocator.nth(idx).click()
}
await page.locator('.agenda-personalize .agenda-personalize-area >> nth=0 >> .agenda-personalize-areamain:first-child > button').click()
for (let idx = 0; idx < areaGroupsCount; idx++) {
await expect(areaGroupsLocator.nth(idx)).not.toHaveClass(/is-checked/)
}
// Test Clear Selection
const groupsLocator = page.locator('.agenda-personalize .agenda-personalize-group')
const groupsCount = await groupsLocator.count()
const randGroupRange = _.take(shuffle(_.range(groupsCount)), 10)
for (const idx of randGroupRange) {
await groupsLocator.nth(idx).click()
}
await page.locator('.agenda-personalize .agenda-personalize-actions > button').first().click()
await expect(page.locator('.agenda-personalize .agenda-personalize-group.is-checked')).toHaveCount(0)
// Click Cancel should hide dialog
await page.locator('.agenda-personalize .agenda-personalize-actions > button').nth(1).click()
await expect(page.locator('.agenda-personalize')).not.toBeVisible()
})
// -> PICK SESSIONS
test('agenda individual sessions picker', async ({ page }) => {
const pickBtnLocator = page.locator('#agenda-quickaccess-picksessions-btn')
const applyBtnLocator = page.locator('#agenda-quickaccess-applypick-btn')
const modifyBtnLocator = page.locator('#agenda-quickaccess-modifypick-btn')
const discardBtnLocator = page.locator('#agenda-quickaccess-discardpick-btn')
const checkboxesLocator = page.locator('.agenda .agenda-table-cell-check > .n-checkbox')
const checkedboxesLocator = page.locator('.agenda .agenda-table-cell-check > .n-checkbox.n-checkbox--checked')
const uncheckedboxesLocator = page.locator('.agenda .agenda-table-cell-check > .n-checkbox:not(.n-checkbox--checked)')
const eventsLocator = page.locator('.agenda .agenda-table-display-event')
// Enter pick mode
await expect(pickBtnLocator).toBeVisible()
await pickBtnLocator.click()
await expect(pickBtnLocator).not.toBeVisible()
await expect(applyBtnLocator).toBeVisible()
await expect(discardBtnLocator).toBeVisible()
// Pick 10 random sessions
await expect(checkboxesLocator).toHaveCount(meetingData.schedule.length)
const randSessionsRange = _.take(shuffle(_.range(meetingData.schedule.length)), 10)
for (const idx of randSessionsRange) {
await checkboxesLocator.nth(idx).click()
}
await applyBtnLocator.click()
await expect(applyBtnLocator).not.toBeVisible()
await expect(modifyBtnLocator).toBeVisible()
await expect(discardBtnLocator).toBeVisible()
await expect(eventsLocator).toHaveCount(10)
// Change selection (keep existing 5 + add 5 new ones)
await modifyBtnLocator.click()
await expect(modifyBtnLocator).not.toBeVisible()
await expect(applyBtnLocator).toBeVisible()
await expect(discardBtnLocator).toBeVisible()
await expect(checkboxesLocator).toHaveCount(meetingData.schedule.length)
await expect(checkedboxesLocator).toHaveCount(10)
for (let idx = 0; idx < 5; idx++) {
await checkedboxesLocator.nth(idx).click()
}
const uncheckedCount = await uncheckedboxesLocator.count()
const uncheckedRandRange = _.take(shuffle(_.range(uncheckedCount - 1)), 5)
for (const idx of uncheckedRandRange) {
await uncheckedboxesLocator.nth(idx).click()
}
await applyBtnLocator.click()
await expect(eventsLocator).toHaveCount(10)
// Discard should clear selection
await discardBtnLocator.click()
await expect(discardBtnLocator).not.toBeVisible()
await expect(modifyBtnLocator).not.toBeVisible()
await expect(pickBtnLocator).toBeVisible()
await expect(page.locator('.agenda .agenda-table-cell-check')).toHaveCount(0)
await expect(eventsLocator).toHaveCount(meetingData.schedule.length)
})
// -> CALENDAR VIEW
test('agenda calendar view', async ({ page }) => {
const diagHeaderLocator = page.locator('.agenda-calendar .agenda-calendar-actions')
const tzButtonsLocator = diagHeaderLocator.locator('.n-button-group button')
const calHintLocator = page.locator('.agenda-calendar-hint > div')
// Open dialog
await page.locator('#agenda-quickaccess-calview-btn').click()
await expect(page.locator('.agenda-calendar')).toBeVisible()
// Check header elements
await expect(page.locator('.agenda-calendar .n-drawer-header__main > span')).toContainText('Calendar View')
await expect(diagHeaderLocator.locator('> button')).toHaveCount(2)
await expect(diagHeaderLocator.locator('> button').first()).toContainText('Filter')
await expect(diagHeaderLocator.locator('> button').last()).toContainText('Close')
// -----------------------
// Check timezone controls
// -----------------------
await expect(diagHeaderLocator.locator('small').first()).toContainText('Timezone')
// Switch to local timezone
await tzButtonsLocator.nth(1).click()
await expect(tzButtonsLocator.nth(1)).toHaveClass(/n-button--primary-type/)
await expect(tzButtonsLocator.first()).not.toHaveClass(/n-button--primary-type/)
const localDateTime = DateTime.fromISO(meetingData.meeting.updated)
.setZone(BROWSER_TIMEZONE)
.setLocale(BROWSER_LOCALE)
.toFormat('DD \'at\' T ZZZZ')
await expect(page.locator('.agenda h6').first()).toContainText(localDateTime)
// Switch to UTC
await tzButtonsLocator.last().click()
await expect(tzButtonsLocator.last()).toHaveClass(/n-button--primary-type/)
await expect(tzButtonsLocator.nth(1)).not.toHaveClass(/n-button--primary-type/)
const utcDateTime = DateTime.fromISO(meetingData.meeting.updated)
.setZone('utc')
.setLocale(BROWSER_LOCALE)
.toFormat('DD \'at\' T ZZZZ')
await expect(page.locator('.agenda h6').first()).toContainText(utcDateTime)
// Switch back to meeting timezone
await tzButtonsLocator.first().click()
await expect(tzButtonsLocator.first()).toHaveClass(/n-button--primary-type/)
// ----------------------
// Check Filters Shortcut
// ----------------------
await diagHeaderLocator.locator('> button').first().click()
// Only check whether the dialog is shown. We already tested the dialog earlier.
await expect(page.locator('.agenda-personalize')).toBeVisible()
// Close dialog
await page.locator('.agenda-personalize .agenda-personalize-actions > button').nth(1).click()
await expect(page.locator('.agenda-personalize')).not.toBeVisible()
// ------------------
// 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
}
await page.route(`**/api/meeting/session/${firstEvent.sessionId}/materials`, route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(materialsInfo)
})
})
await page.route(materialsUrl, route => {
route.fulfill({
status: 200,
contentType: 'text/plain',
body: 'The internet is a series of tubes.'
})
})
await page.locator('.agenda-calendar .fc-event').first().click()
// Only check whether the dialog is shown. We already tested the dialog earlier.
await expect(page.locator('.agenda-eventdetails')).toBeVisible()
// Close dialog
await page.locator('.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')}`
await page.locator('.agenda-calendar .fc-event').first().hover()
await expect(calHintLocator.first()).toContainText(firstEvent.name)
await expect(calHintLocator.nth(1)).toContainText(firstEvent.location.short)
await expect(calHintLocator.nth(1)).toContainText(firstEvent.room)
await expect(calHintLocator.nth(2)).toContainText(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')}`
await page.locator('.agenda-calendar .fc-event').nth(1).hover()
await expect(calHintLocator.first()).toContainText(secondEvent.name)
await expect(calHintLocator.nth(1)).toContainText(secondEvent.location.short)
await expect(calHintLocator.nth(1)).toContainText(secondEvent.room)
await expect(calHintLocator.nth(2)).toContainText(hoverDateTime)
// ------------------------------
// Click Close should hide dialog
// ------------------------------
await diagHeaderLocator.locator('button').last().click()
await expect(page.locator('.agenda-calendar')).not.toBeVisible()
})
// -> SETTINGS DIALOG
test('agenda settings', async ({ page, browserName }) => {
// Open dialog
await page.locator('.agenda-topnav-right > button:last-child').click()
await expect(page.locator('.agenda-settings')).toBeVisible()
// Check header elements
await expect(page.locator('.agenda-settings .n-drawer-header__main > span')).toContainText('Agenda Settings')
await expect(page.locator('.agenda-settings .agenda-settings-actions > button')).toHaveCount(2)
await expect(page.locator('.agenda-settings .agenda-settings-actions > button').first()).toBeVisible()
await expect(page.locator('.agenda-settings .agenda-settings-actions > button').last()).toContainText('Close')
// -------------------
// Check export config
// -------------------
await page.locator('.agenda-settings .agenda-settings-actions > button').first().click()
const [download] = await Promise.all([
page.waitForEvent('download'),
page.locator('.n-dropdown-option:has-text("Export Configuration")').click()
])
const downloadPath = await download.path()
try {
const downloadedConfig = JSON.parse(await fs.readFile(downloadPath, 'utf8'))
const expectedConfig = JSON.parse(await fs.readFile('data/agenda-settings.json', 'utf8'))
await expect(downloadedConfig).toEqual(expectedConfig)
} catch (err) {
expect(err).toBeUndefined()
}
// -------------------
// Check import config
// -------------------
await test.step('import config', async () => {
if (browserName === 'chromium') {
// Chromium use the experimental file selector API so this test won't work, skipping...
// See https://github.com/microsoft/playwright/issues/8850')
return
}
await page.locator('.agenda-settings .agenda-settings-actions > button').first().click()
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'),
page.locator('.n-dropdown-option:has-text("Import Configuration")').click()
])
await fileChooser.setFiles('data/agenda-settings.json')
await expect(page.locator('.n-message')).toContainText('Config imported successfully')
})
// -----------------------
// Check timezone controls
// -----------------------
const tzMeetingBtnLocator = page.locator('#agenda-settings-tz-btn button:first-child')
const tzLocalBtnLocator = page.locator('#agenda-settings-tz-btn button:nth-child(2)')
const tzUtcBtnLocator = page.locator('#agenda-settings-tz-btn button:last-child')
await expect(page.locator('.agenda-settings-content > .n-divider').first()).toContainText('Timezone')
// Switch to local timezone
await tzLocalBtnLocator.click()
await expect(tzLocalBtnLocator).toHaveClass(/n-button--primary-type/)
await expect(tzMeetingBtnLocator).not.toHaveClass(/n-button--primary-type/)
const localDateTime = DateTime.fromISO(meetingData.meeting.updated)
.setZone(BROWSER_TIMEZONE)
.setLocale(BROWSER_LOCALE)
.toFormat('DD \'at\' T ZZZZ')
await expect(page.locator('.agenda h6').first()).toContainText(localDateTime)
// Switch to UTC
await tzUtcBtnLocator.click()
await expect(tzUtcBtnLocator).toHaveClass(/n-button--primary-type/)
await expect(tzLocalBtnLocator).not.toHaveClass(/n-button--primary-type/)
const utcDateTime = DateTime.fromISO(meetingData.meeting.updated)
.setZone('utc')
.setLocale(BROWSER_LOCALE)
.toFormat('DD \'at\' T ZZZZ')
await expect(page.locator('.agenda h6').first()).toContainText(utcDateTime)
// Switch back to meeting timezone
await tzMeetingBtnLocator.click()
await expect(tzMeetingBtnLocator).toHaveClass(/n-button--primary-type/)
await expect(page.locator('#agenda-settings-tz-ddn')).toContainText('Tokyo')
// ----------------------
// Check display controls
// ----------------------
await expect(page.locator('.agenda-settings-content > .n-divider').nth(1)).toContainText('Display')
// -> Test Current Meeting Info Note toggle
const infonoteSwitchLocator = page.locator('#agenda-settings-tgl-infonote div[role=switch]')
await infonoteSwitchLocator.click()
await expect(page.locator('.agenda .agenda-infonote')).not.toBeVisible()
await infonoteSwitchLocator.click()
await expect(page.locator('.agenda .agenda-infonote')).toBeVisible()
// -> Test Event Icons toggle
const eventiconsSwitchLocator = page.locator('#agenda-settings-tgl-eventicons div[role=switch]')
await eventiconsSwitchLocator.click()
await expect(page.locator('.agenda .agenda-event-icon')).toHaveCount(0)
await eventiconsSwitchLocator.click()
await expect(page.locator('.agenda .agenda-event-icon')).not.toHaveCount(0)
// -> Test Floor Indicators toggle
const floorindSwitchLocator = page.locator('#agenda-settings-tgl-floorind div[role=switch]')
await floorindSwitchLocator.click()
await expect(page.locator('.agenda .agenda-table-cell-room > span.badge')).toHaveCount(0)
await floorindSwitchLocator.click()
await expect(page.locator('.agenda .agenda-table-cell-room > span.badge')).not.toHaveCount(0)
// -> Test Group Area Indicators toggle
const groupindSwitchLocator = page.locator('#agenda-settings-tgl-groupind div[role=switch]')
await groupindSwitchLocator.click()
await expect(page.locator('.agenda .agenda-table-cell-group > span.badge')).toHaveCount(0)
await groupindSwitchLocator.click()
await expect(page.locator('.agenda .agenda-table-cell-group > span.badge')).not.toHaveCount(0)
// -> Test Bolder Text toggle
const boldertxtSwitchLocator = page.locator('#agenda-settings-tgl-boldertxt div[role=switch]')
await boldertxtSwitchLocator.click()
await expect(page.locator('.agenda')).toHaveClass(/bolder-text/)
await boldertxtSwitchLocator.click()
await expect(page.locator('.agenda')).not.toHaveClass(/bolder-text/)
// ----------------------------
// Check calendar view controls
// ----------------------------
await expect(page.locator('.agenda-settings-content > .n-divider').nth(2)).toContainText('Calendar View')
// TODO: calendar view checks
// ----------------------------
// Check calendar view controls
// ----------------------------
await expect(page.locator('.agenda-settings-content > .n-divider').nth(3)).toContainText('Custom Colors / Tags')
// ------------------------------
// Click Close should hide dialog
// ------------------------------
await page.locator('.agenda-settings .agenda-settings-actions > button').last().click()
await expect(page.locator('.agenda-settings')).not.toBeVisible()
})
// -> SHARE DIALOG
test('agenda share dialog', async ({ page }) => {
// Open dialog
await page.locator('.agenda-topnav-right > button:nth-child(2)').click()
await expect(page.locator('.agenda-share')).toBeVisible()
// Check header elements
await expect(page.locator('.agenda-share .n-card-header__main > .agenda-share-header > .bi')).toBeVisible()
await expect(page.locator('.agenda-share .n-card-header__main > .agenda-share-header > .bi + span')).toContainText('Share this view')
// Check input URL
await expect(page.locator('.agenda-share .agenda-share-content input[type=text]')).toHaveValue(`http://localhost:3000/meeting/${meetingData.meeting.number}/agenda`)
// Clicking X should close the dialog
await page.locator('.agenda-share .n-card-header__extra > .agenda-share-header > button').click()
await expect(page.locator('.agenda-share')).not.toBeVisible()
})
// -> ADD TO CALENDAR
test('agenda add to calendar', async ({ page }) => {
await expect(page.locator('#agenda-quickaccess-addtocal-btn')).toContainText('Add to your calendar')
await page.locator('#agenda-quickaccess-addtocal-btn').click()
const ddnLocator = page.locator('.n-dropdown-menu > div > a.agenda-quickaccess-callinks')
await expect(ddnLocator).toHaveCount(2)
await expect(ddnLocator.first()).toContainText('Subscribe')
await expect(ddnLocator.last()).toContainText('Download')
// Intercept Download ICS Call
await page.route(`**/meeting/${meetingData.meeting.number}/agenda.ics`, route => {
route.fulfill({
status: 200,
contentType: 'text/calendar',
headers: {
'Content-disposition': 'attachment; filename=agenda.ics'
},
body: 'test'
})
})
// Cannot test if webcal link works because external app handling not supported:
// See https://github.com/microsoft/playwright/issues/11014
// Test Download ICS
const [download] = await Promise.all([
page.waitForEvent('download'),
ddnLocator.nth(1).click()
])
const downloadPath = await download.path()
try {
const testIcs = await fs.readFile(downloadPath, 'utf8')
await expect(testIcs).toEqual('test')
} catch (err) {
expect(err).toBeUndefined()
}
})
// -> JUMP TO DAY
test('agenda jump to specific days', async ({ page, browserName }) => {
// -> Separator label
await expect(page.locator('div[role=separator]:above(.agenda .agenda-quickaccess-jumpto)').first()).toContainText('Jump to...')
// -> Check nav items
const navItemLocator = page.locator('.agenda .agenda-quickaccess-jumpto > .nav-item')
await expect(navItemLocator).toHaveCount(7)
for (let idx = 0; idx < 7; idx++) {
const localDateTime = DateTime.fromISO(meetingData.meeting.startDate, { zone: BROWSER_TIMEZONE })
.setZone(BROWSER_TIMEZONE)
.setLocale(BROWSER_LOCALE)
.plus({ days: idx })
.toLocaleString(DateTime.DATE_HUGE)
await expect(navItemLocator.nth(idx)).toContainText(localDateTime)
}
// -> Jump to specific days
if (browserName === 'chromium') {
// Exclude firefox as this test doesn't run reliably on it in CI
for (const idx of [6, 1, 5]) {
await navItemLocator.nth(idx).locator('a').click()
await setTimeout(2500)
await expect(await commonHelper.isIntersectingViewport(page, `.agenda-table-display-day >> nth=${idx}`)).toBeTruthy()
}
}
})
// -> Color Tagging
test('agenda colors/tags assignment', async ({ page }) => {
test.slow() // Triple the default timeout
const openBtnLocator = page.locator('.agenda .agenda-table-colorpicker')
const colorLgdLocator = page.locator('.agenda .agenda-colorlegend')
const eventRowsLocator = page.locator('.agenda .agenda-table-display-event')
const colorLgdSwitchLocator = page.locator('#agenda-settings-tgl-colorlgd div[role=switch]')
const colorNamesIptLocator = page.locator('.agenda-settings-colors-row .n-input')
const randColorNames = _.times(5, faker.music.genre)
await expect(openBtnLocator).toBeVisible()
await openBtnLocator.click()
// Check Legend
await expect(colorLgdLocator).toBeVisible()
await expect(colorLgdLocator.locator('> *')).toHaveCount(6)
await expect(colorLgdLocator.locator('> * >> nth=0')).toContainText('Color Legend')
// Check color dots
await expect(page.locator('.agenda .agenda-table-display-event .agenda-table-colorindicator.is-active')).toHaveCount(meetingData.schedule.length)
// -------------------------
// Assign colors to sessions
// -------------------------
for (let idx = 0; idx < 5; idx++) {
await expect(eventRowsLocator.nth(idx).locator('.agenda-table-colorindicator')).toBeVisible()
await eventRowsLocator.nth(idx).locator('.agenda-table-colorindicator').click()
await expect(eventRowsLocator.nth(idx).locator('.agenda-table-colorchoices')).toBeVisible()
await expect(eventRowsLocator.nth(idx).locator('.agenda-table-colorchoices > .agenda-table-colorchoice')).toHaveCount(6)
await eventRowsLocator.nth(idx).locator('.agenda-table-colorchoices > .agenda-table-colorchoice').nth(idx + 1).click()
await expect(eventRowsLocator.nth(idx).locator('.agenda-table-colorchoices')).not.toBeVisible()
}
// Exit color assignment mode
await openBtnLocator.click()
await expect(page.locator('.agenda .agenda-table-display-event .agenda-table-colorindicator')).toHaveCount(5)
await expect(page.locator('.agenda .agenda-table-display-event .agenda-table-colorindicator.is-active')).toHaveCount(0)
await expect(colorLgdLocator).toBeVisible()
// ----------------------------------------
// Change color legend from settings dialog
// ----------------------------------------
// Open dialog
await page.locator('.agenda-topnav-right > button:last-child').click()
await expect(page.locator('.agenda-settings')).toBeVisible()
// Toggle color legend switch
await colorLgdSwitchLocator.click()
// Legend should be hidden
await expect(colorLgdLocator).not.toBeVisible()
// Toggle color legend back
await colorLgdSwitchLocator.click()
// Legend should be visible
await expect(colorLgdLocator).toBeVisible()
// Change color names
for (let idx = 0; idx < 5; idx++) {
await colorNamesIptLocator.nth(idx).locator('input').fill(randColorNames[idx])
await setTimeout(1000) // Account for change debounce
await expect(colorLgdLocator.locator(`> * >> nth=${idx + 1}`)).toContainText(randColorNames[idx])
}
// Close dialog
await page.locator('.agenda-settings .agenda-settings-actions > button').last().click()
await expect(page.locator('.agenda-settings')).not.toBeVisible()
// ---------------
// Unassign colors
// ---------------
// Re-enter color assignment mode
await openBtnLocator.click()
// Remove color selection
for (let idx = 0; idx < 5; idx++) {
await expect(eventRowsLocator.nth(idx).locator('.agenda-table-colorindicator')).toBeVisible()
await eventRowsLocator.nth(idx).locator('.agenda-table-colorindicator').click()
await expect(eventRowsLocator.nth(idx).locator('.agenda-table-colorchoices')).toBeVisible()
await expect(eventRowsLocator.nth(idx).locator('.agenda-table-colorchoices > .agenda-table-colorchoice')).toHaveCount(6)
await eventRowsLocator.nth(idx).locator('.agenda-table-colorchoices > .agenda-table-colorchoice').first().click()
await expect(eventRowsLocator.nth(idx).locator('.agenda-table-colorchoices')).not.toBeVisible()
}
// Exit color assignment mode
await openBtnLocator.click()
// No colored dots should appear
await expect(page.locator('.agenda .agenda-table-display-event .agenda-table-colorindicator')).toHaveCount(0)
// Clear all colors from Settings menu
await page.locator('.agenda-topnav-right > button:last-child').click()
await expect(page.locator('.agenda-settings')).toBeVisible()
await page.locator('.agenda-settings .agenda-settings-actions > button').first().click()
await page.locator('.n-dropdown-option:has-text("Clear Color")').click()
// Color legend should no longer be displayed
await expect(colorLgdLocator).not.toBeVisible()
await expect(page.locator('.agenda-settings')).not.toBeVisible()
})
})
// ====================================================================
// AGENDA (future meeting) | DESKTOP viewport
// ====================================================================
test.describe('future - desktop', () => {
let meetingData
test.beforeAll(async () => {
// Generate meeting data
meetingData = meetingHelper.generateAgendaResponse({ dateMode: 'future' })
})
test.beforeEach(async ({ page }) => {
// Intercept Meeting Data API
await page.route(`**/api/meeting/${meetingData.meeting.number}/agenda-data`, route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(meetingData)
})
})
await page.setViewportSize({
width: viewports.desktop[0],
height: viewports.desktop[1]
})
// Visit agenda page and await Meeting Data API call to complete
await Promise.all([
page.waitForResponse(`**/api/meeting/${meetingData.meeting.number}/agenda-data`),
page.goto(`/meeting/${meetingData.meeting.number}/agenda`)
])
// Wait for page to be ready
await page.locator('.agenda h1').waitFor({ state: 'visible' })
await setTimeout(500)
})
// -> SCHEDULE LIST -> Warning
test('has current meeting warning', async ({ page }) => {
await expect(page.locator('.agenda .agenda-currentwarn')).toContainText('Note: IETF agendas are subject to change, up to and during a meeting.')
})
// -> SCHEDULE LIST -> Table Events
test('has schedule list table events', async ({ page }) => {
test.slow() // Triple the default timeout
const eventRowsLocator = page.locator('.agenda-table .agenda-table-display-event')
await expect(eventRowsLocator).toHaveCount(meetingData.schedule.length)
for (let idx = 0; idx < meetingData.schedule.length; idx++) {
const row = eventRowsLocator.nth(idx)
const event = meetingData.schedule[idx]
// -----------------------
// Buttons / Status Column
// -----------------------
if (event.status === 'sched') {
const eventButtons = row.locator('.agenda-table-cell-links > .agenda-table-cell-links-buttons')
if (event.flags.showAgenda || (['regular', 'plenary', 'other'].includes(event.type) && !['admin', 'closed_meeting', 'officehours', 'social'].includes(event.purpose))) {
if (event.flags.agenda) {
// Show meeting materials button
await expect(eventButtons.locator('i.bi.bi-collection')).toBeVisible()
// ZIP materials button
await expect(eventButtons.locator(`#btn-lnk-${event.id}-tar`)).toHaveAttribute('href', `/meeting/${meetingData.meeting.number}/agenda/${event.acronym}-drafts.tgz`)
await expect(eventButtons.locator(`#btn-lnk-${event.id}-tar > i.bi`)).toBeVisible()
// PDF materials button
await expect(eventButtons.locator(`#btn-lnk-${event.id}-pdf`)).toHaveAttribute('href', `/meeting/${meetingData.meeting.number}/agenda/${event.acronym}-drafts.pdf`)
await expect(eventButtons.locator(`#btn-lnk-${event.id}-pdf > i.bi`)).toBeVisible()
} else if (event.type === 'regular') {
// No meeting materials yet warning badge
await expect(eventButtons.locator('.no-meeting-materials')).toBeVisible()
}
if (event.name.toLowerCase().includes('hackathon')) {
// Hackathon Wiki button
const hackathonWikiLink = `https://wiki.ietf.org/meeting/${meetingData.meeting.number}/hackathon`
await expect(eventButtons.locator(`#btn-lnk-${event.id}-wiki`)).toHaveAttribute('href', hackathonWikiLink)
await expect(eventButtons.locator(`#btn-lnk-${event.id}-wiki > i.bi`)).toBeVisible()
} else {
// Notepad button
const hedgeDocLink = `https://notes.ietf.org/notes-ietf-${meetingData.meeting.number}-${event.type === 'plenary' ? 'plenary' : event.acronym}`
await expect(eventButtons.locator(`#btn-lnk-${event.id}-note`)).toHaveAttribute('href', hedgeDocLink)
await expect(eventButtons.locator(`#btn-lnk-${event.id}-note > i.bi`)).toBeVisible()
}
// Chat room
await expect(eventButtons.locator(`#btn-lnk-${event.id}-room`)).toHaveAttribute('href', event.links.chat)
await expect(eventButtons.locator(`#btn-lnk-${event.id}-room > i.bi`)).toBeVisible()
// Video Stream
if (event.links.videoStream) {
await expect(eventButtons.locator(`#btn-lnk-${event.id}-video`)).toHaveAttribute('href', meetingHelper.formatLinkUrl(event.links.videoStream, event, meetingData.meeting.number))
await expect(eventButtons.locator(`#btn-lnk-${event.id}-video > i.bi`)).toBeVisible()
}
// Onsite Tool
if (event.links.onsitetool) {
await expect(eventButtons.locator(`#btn-lnk-${event.id}-onsitetool`)).toHaveAttribute('href', meetingHelper.formatLinkUrl(event.links.onsitetool, event, meetingData.meeting.number))
await expect(eventButtons.locator(`#btn-lnk-${event.id}-onsitetool > i.bi`)).toBeVisible()
}
// Audio Stream
if (event.links.audioStream) {
await expect(eventButtons.locator(`#btn-lnk-${event.id}-audio`)).toHaveAttribute('href', meetingHelper.formatLinkUrl(event.links.audioStream, event, meetingData.meeting.number))
await expect(eventButtons.locator(`#btn-lnk-${event.id}-audio > i.bi`)).toBeVisible()
}
// Remote Call-In
let remoteCallInUrl = null
if (event.note) {
remoteCallInUrl = meetingHelper.findFirstConferenceUrl(event.note)
}
if (!remoteCallInUrl && event.remoteInstructions) {
remoteCallInUrl = meetingHelper.findFirstConferenceUrl(event.remoteInstructions)
}
if (!remoteCallInUrl && event.links.webex) {
remoteCallInUrl = event.links.webex
}
if (remoteCallInUrl) {
await expect(eventButtons.locator(`#btn-lnk-${event.id}-remotecallin`)).toHaveAttribute('href', remoteCallInUrl)
await expect(eventButtons.locator(`#btn-lnk-${event.id}-remotecallin > i.bi`)).toBeVisible()
}
// calendar
if (event.links.calendar) {
await expect(eventButtons.locator(`#btn-lnk-${event.id}-calendar`)).toHaveAttribute('href', event.links.calendar)
await expect(eventButtons.locator(`#btn-lnk-${event.id}-calendar > i.bi`)).toBeVisible()
}
} else {
await expect(eventButtons).toHaveCount(0)
}
}
}
})
})
// ====================================================================
// AGENDA (live meeting) | DESKTOP viewport
// ====================================================================
test.describe('live - desktop', () => {
let meetingData
const currentTime = DateTime.fromISO('2022-02-01T13:45:15', { zone: 'Asia/Tokyo' })
const liveEvents = []
let lastLiveEvent = null
test.beforeAll(async () => {
// Generate meeting data
meetingData = meetingHelper.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
}
}
})
test.beforeEach(async ({ page }) => {
// Intercept Meeting Data API
await page.route(`**/api/meeting/${meetingData.meeting.number}/agenda-data`, route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(meetingData)
})
})
await page.setViewportSize({
width: viewports.desktop[0],
height: viewports.desktop[1]
})
// Override Date in page to fixed time
await page.addInitScript(`{
// Extend Date constructor to default to fixed time
Date = class extends Date {
constructor(...args) {
if (args.length === 0) {
super(${currentTime.toMillis()});
} else {
super(...args);
}
}
}
// Override Date.now() to start from fixed time
const __DateNowOffset = ${currentTime.toMillis()} - Date.now();
const __DateNow = Date.now;
Date.now = () => __DateNow() + __DateNowOffset;
}`)
// Visit agenda page and await Meeting Data API call to complete
await Promise.all([
page.waitForResponse(`**/api/meeting/${meetingData.meeting.number}/agenda-data`),
page.goto(`/meeting/${meetingData.meeting.number}/agenda`)
])
// Wait for page to be ready
await page.locator('.agenda h1').waitFor({ state: 'visible' })
await setTimeout(500)
})
// -> LIVE MEETING ELEMENTS
test('live meeting elements', async ({ page }) => {
const navItemsLocator = page.locator('.agenda .agenda-quickaccess-jumpto > .nav-item')
// Highlighted Live Sessions
await expect(page.locator('.agenda .agenda-table-display-event.agenda-table-live')).toHaveCount(liveEvents.length)
// Live Red Line
await expect(page.locator('.agenda .agenda-table-redhand')).toBeVisible()
const expectedOffsetTop = await page.locator(`#agenda-rowid-${lastLiveEvent.id}`).evaluate(node => node.offsetTop)
const offsetTop = await page.locator('.agenda .agenda-table-redhand').evaluate(node => node.offsetTop)
const isCloseEnough = offsetTop >= expectedOffsetTop - 15 && offsetTop <= expectedOffsetTop + 15
await expect(isCloseEnough).toBeTruthy()
// Jump to Now
await expect(navItemsLocator).toHaveCount(8)
await expect(navItemsLocator.first()).toContainText('Now')
await navItemsLocator.first().click()
await setTimeout(2500)
// red line position isn't pixel perfect on CI, so accept some range
const redlineBoundingBox = await page.locator('.agenda .agenda-table-redhand').boundingBox()
await expect(redlineBoundingBox.y >= -20 && redlineBoundingBox.y <= 20).toBeTruthy()
})
// -> HIDE RED LINE
test('live red line toggle', async ({ page }) => {
// Open settings dialog
await page.locator('.agenda-topnav-right > button:last-child').click()
await expect(page.locator('.agenda-settings')).toBeVisible()
// Toggle red line switch
const redlineSwitchLocator = page.locator('#agenda-settings-tgl-redline div[role=switch]')
await redlineSwitchLocator.click()
await expect(page.locator('.agenda .agenda-table-redhand')).not.toBeVisible()
await redlineSwitchLocator.click()
await expect(page.locator('.agenda .agenda-table-redhand')).toBeVisible()
// Close dialog
await page.locator('.agenda-settings .agenda-settings-actions > button').last().click()
await expect(page.locator('.agenda-settings')).not.toBeVisible()
})
})
// ====================================================================
// AGENDA (past meeting) | SMALL DESKTOP/TABLET/MOBILE viewports
// ====================================================================
test.describe('past - small screens', () => {
let meetingData
test.beforeAll(async () => {
// Generate meeting data
meetingData = meetingHelper.generateAgendaResponse({ dateMode: 'past' })
})
for (const vp of ['smallDesktop', 'tablet', 'mobile']) {
test(vp, async ({ page }) => {
// Intercept Meeting Data API
await page.route(`**/api/meeting/${meetingData.meeting.number}/agenda-data`, route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(meetingData)
})
})
await page.setViewportSize({
width: viewports[vp][0],
height: viewports[vp][1]
})
// Visit agenda page and await Meeting Data API call to complete
await Promise.all([
page.waitForResponse(`**/api/meeting/${meetingData.meeting.number}/agenda-data`),
page.goto(`/meeting/${meetingData.meeting.number}/agenda`)
])
// Wait for page to be ready
await page.locator('.agenda h1').waitFor({ state: 'visible' })
await setTimeout(500)
// -> NARROW QUICK ACCESS PANEL (smallDesktop only)
if (vp === 'smallDesktop') {
// Alternate labels for buttons
await expect(page.locator('#agenda-quickaccess-filterbyareagroups-btn')).toContainText('Filter...')
await expect(page.locator('#agenda-quickaccess-filterbyareagroups-btn + button')).toContainText('Pick...')
await expect(page.locator('#agenda-quickaccess-calview-btn')).toContainText('Cal View')
await expect(page.locator('#agenda-quickaccess-calview-btn + button')).toContainText('.ics')
// -> Shorter date labels for Jump to buttons
const jumpNavLocator = page.locator('.agenda .agenda-quickaccess-jumpto > .nav-item')
await expect(jumpNavLocator).toHaveCount(7)
for (let idx = 0; idx < 7; idx++) {
const localDateTime = DateTime.fromISO(meetingData.meeting.startDate, { zone: meetingData.meeting.timezone })
.setLocale(BROWSER_LOCALE)
.plus({ days: idx })
.toFormat('ccc LLL d')
await expect(jumpNavLocator.nth(idx)).toContainText(localDateTime)
await expect(jumpNavLocator.nth(idx).locator('i.bi')).not.toBeVisible()
}
}
// Check for elements that should not exist on smaller screens
if (vp === 'tablet' || vp === 'mobile') {
// has no updated date
await expect(page.locator('.agenda > h4 > h6')).not.toBeVisible()
// has no timezone dropdown selector
await expect(page.locator('.agenda .agenda-tz-selector + .agenda-timezone-ddn')).not.toBeVisible()
// has no floor + group indicators
const floorIndLocator = page.locator('.agenda .agenda-table-cell-room > .badge')
const floorIndCount = await floorIndLocator.count()
for (let idx = 0; idx < floorIndCount; idx++) {
await expect(floorIndLocator.nth(idx)).not.toBeVisible()
}
await expect(page.locator('.agenda .agenda-table-cell-group > .badge')).toHaveCount(0)
// Session buttons should be hidden in a dropdown menu
const linkBtnsLocator = page.locator('.agenda .agenda-table-display-event .agenda-table-cell-links-buttons')
const linkBtnsCount = await linkBtnsLocator.count()
for (let idx; idx < linkBtnsCount; idx++) {
await expect(linkBtnsLocator.nth(idx).locator('> *')).toHaveCount(1)
}
// TODO: Check for dropdown links once changed to a custom panel with standard links
// Bottom Mobile Bar
const barBtnLocator = page.locator('.agenda-mobile-bar > button')
// has no lateral quick access panel
await expect(page.locator('.agenda-quickaccess')).not.toBeVisible()
// has a bottom mobile bar
await expect(page.locator('.agenda-mobile-bar')).toBeVisible()
await expect(barBtnLocator).toHaveCount(5)
// can open the jump to day dropdown
await barBtnLocator.first().click()
const jumpDayDdnLocator = page.locator('.n-dropdown-menu [data-testid=mobile-link]')
await expect(jumpDayDdnLocator).toHaveCount(7)
for (let idx = 0; idx < 7; idx++) {
const localDateTime = DateTime.fromISO(meetingData.meeting.startDate, { zone: meetingData.meeting.timezone })
.setLocale(BROWSER_LOCALE)
.plus({ days: idx })
.toFormat('ccc LLL d')
await expect(jumpDayDdnLocator.nth(idx)).toContainText(`Jump to ${localDateTime}`)
}
// can open the filters overlay
await barBtnLocator.nth(1).click()
await expect(page.locator('.agenda-personalize')).toBeVisible()
await page.locator('.agenda-personalize .agenda-personalize-actions > button').nth(1).click()
await expect(page.locator('.agenda-personalize')).toBeHidden()
// can open the calendar view
await barBtnLocator.nth(2).click()
await expect(page.locator('.agenda-calendar')).toBeVisible()
await page.locator('.agenda-calendar .agenda-calendar-actions > button').nth(1).click()
await expect(page.locator('.agenda-calendar')).toBeHidden()
// can open the ics dropdown
await barBtnLocator.nth(3).click()
const calDdnLocator = page.locator('.n-dropdown-menu > .n-dropdown-option')
await expect(calDdnLocator).toHaveCount(2)
await expect(calDdnLocator.first()).toContainText('Subscribe')
await expect(calDdnLocator.last()).toContainText('Download')
// can open the settings overlay
await barBtnLocator.last().click()
await expect(page.locator('.agenda-settings')).toBeVisible()
await page.locator('.agenda-settings .agenda-settings-actions > button').nth(1).click()
await expect(page.locator('.agenda-settings')).toBeHidden()
}
})
}
})