Init commit

This commit is contained in:
ShreejitPanchal
2026-05-19 19:56:02 +08:00
commit 6601501eff
4047 changed files with 2185372 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,12 @@
browser.addCommand('chooseFile', async function (this: WebdriverIO.Element, localFilePath: string) {
try {
const remoteFile = await browser.uploadFile(localFilePath)
return this.addValue(remoteFile)
} catch {
console.log('Cannot upload file, fallback to add value.')
// Firefox does not support upload file, but if we're running the test in local we don't really need it
return this.addValue(localFilePath)
}
}, true)

View File

@@ -0,0 +1,73 @@
import { NSFWPolicyType } from '@peertube/peertube-models'
import { browserSleep, go, setCheckboxEnabled } from '../utils'
export class AdminConfigPage {
async navigateTo (page: 'information' | 'live' | 'general' | 'homepage') {
const url = '/admin/settings/config/' + page
const currentUrl = await browser.getUrl()
if (!currentUrl.endsWith(url)) {
await go(url)
}
await $('a.active[href="' + url + '"]').waitForDisplayed()
}
async updateNSFWSetting (newValue: NSFWPolicyType) {
await this.navigateTo('information')
const elem = $(`#instanceDefaultNSFWPolicy-${newValue} + label`)
await elem.waitForDisplayed()
await elem.scrollIntoView({ block: 'center' }) // Avoid issues with fixed header
await elem.waitForClickable()
return elem.click()
}
async updateHomepage (newValue: string) {
await this.navigateTo('homepage')
return $('#homepageContent').setValue(newValue)
}
async toggleSignup (enabled: boolean) {
await this.navigateTo('general')
return setCheckboxEnabled('signupEnabled', enabled)
}
async toggleSignupApproval (required: boolean) {
await this.navigateTo('general')
return setCheckboxEnabled('signupRequiresApproval', required)
}
async toggleSignupEmailVerification (required: boolean) {
await this.navigateTo('general')
return setCheckboxEnabled('signupRequiresEmailVerification', required)
}
async toggleLive (enabled: boolean) {
await this.navigateTo('live')
return setCheckboxEnabled('liveEnabled', enabled)
}
async save () {
const button = $('my-admin-save-bar .save-button')
try {
await button.waitForClickable()
} catch {
// The config may have not been changed
return
} finally {
await browserSleep(1000) // Wait for the button to be clickable
}
await button.click()
await button.waitForClickable({ reverse: true })
}
}

View File

@@ -0,0 +1,31 @@
import { browserSleep, go } from '../utils'
export class AdminPluginPage {
async navigateToPluginSearch () {
await go('/admin/settings/plugins/search')
await $('my-plugin-search').waitForDisplayed()
}
async search (name: string) {
const input = $('.search-bar input')
await input.waitForDisplayed()
await input.clearValue()
await input.setValue(name)
await browserSleep(1000)
}
async installHelloWorld () {
$('.plugin-name=hello-world').waitForDisplayed()
await $('.card-body my-button[icon=cloud-download]').click()
const submitModalButton = $('.modal-content input[type=submit]')
await submitModalButton.waitForClickable()
await submitModalButton.click()
await $('.card-body my-edit-button').waitForDisplayed()
}
}

View File

@@ -0,0 +1,33 @@
import { browserSleep, findParentElement, go } from '../utils'
export class AdminRegistrationPage {
async navigateToRegistrationsList () {
await go('/admin/moderation/registrations/list')
await $('my-registration-list').waitForDisplayed()
}
async accept (username: string, moderationResponse: string) {
const usernameEl = $('*=' + username)
await usernameEl.waitForDisplayed()
const tr = await findParentElement(usernameEl, async el => await el.getTagName() === 'tr')
await tr.$('.action-cell .dropdown-root').click()
const accept = $('span*=Accept this request')
await accept.waitForClickable()
await accept.click()
const moderationResponseTextarea = $('#moderationResponse')
await moderationResponseTextarea.waitForDisplayed()
await moderationResponseTextarea.setValue(moderationResponse)
const submitButton = $('.modal-footer input[type=submit]')
await submitButton.waitForClickable()
await submitButton.click()
await browserSleep(1000)
}
}

View File

@@ -0,0 +1,24 @@
export class AdminUserPage {
async createUser (options: {
username: string
password: string
}) {
const { username, password } = options
await $('.menu-link[title=Overview]').click()
await $('a*=Create user').click()
await $('#username').waitForDisplayed()
await $('#username').setValue(username)
await $('#password').setValue(password)
await $('#channelName').setValue(`${username}_channel`)
await $('#email').setValue(`${username}@example.com`)
const submit = $('my-user-create .primary-button')
await submit.scrollIntoView()
await submit.waitForClickable()
await submit.click()
await $('.cell-username*=' + username).waitForDisplayed()
}
}

View File

@@ -0,0 +1,45 @@
import { NSFWPolicyType } from '@peertube/peertube-models'
import { getCheckbox } from '../utils'
export class AnonymousSettingsPage {
async openSettings () {
const link = $('my-header .settings-button')
await link.waitForClickable()
await link.click()
await $('my-user-video-settings').waitForDisplayed()
}
async closeSettings () {
const closeModal = $('.modal.show .modal-header > button')
await closeModal.waitForClickable()
await closeModal.click()
await $('.modal.show').waitForDisplayed({ reverse: true })
}
async clickOnP2PCheckbox () {
const p2p = await getCheckbox('p2pEnabled')
await p2p.waitForClickable()
await p2p.click()
}
async updateNSFW (newValue: NSFWPolicyType) {
const nsfw = $(`#nsfwPolicy-${newValue} + label`)
await nsfw.waitForClickable()
await nsfw.click()
await $(`#nsfwPolicy-${newValue}:checked`).waitForExist()
}
async updateViolentFlag (newValue: NSFWPolicyType) {
const nsfw = $(`#nsfwFlagViolent-${newValue} + label`)
await nsfw.waitForClickable()
await nsfw.click()
await $(`#nsfwFlagViolent-${newValue}:checked`).waitForExist()
}
}

View File

@@ -0,0 +1,97 @@
import { browserSleep, go, isAndroid } from '../utils'
export class LoginPage {
constructor (private isMobileDevice: boolean) {
}
async login (options: {
username: string
password: string
displayName?: string
url?: string
}) {
const { username, password, url = '/login', displayName = username } = options
await go(url)
await browser.execute(`window.localStorage.setItem('no_account_setup_warning_modal', 'true')`)
await browser.execute(`window.localStorage.setItem('no_instance_config_warning_modal', 'true')`)
await browser.execute(`window.localStorage.setItem('no_welcome_modal', 'true')`)
await $('input#username').setValue(username)
await $('input#password').setValue(password)
await browserSleep(1000)
const submit = $('.login-form-and-externals > form input[type=submit]')
await submit.click()
// Have to do this on Android, don't really know why
// I think we need to "escape" from the password input, so click twice on the submit button
if (isAndroid()) {
await browserSleep(2000)
await submit.click()
}
await this.ensureIsLoggedInAs(displayName)
}
async getLoginError (username: string, password: string) {
await go('/login')
await $('input#username').setValue(username)
await $('input#password').setValue(password)
await browser.pause(1000)
await $('form input[type=submit]').click()
return $('.alert-danger').getText()
}
loginAsRootUser () {
return this.login({ username: 'root', password: this.getRootPassword() })
}
getRootPassword () {
return 'test' + this.getSuffix()
}
loginOnPeerTube2 () {
if (!process.env.PEERTUBE2_E2E_PASSWORD) {
throw new Error('PEERTUBE2_E2E_PASSWORD env is missing for user e2e on peertube2.cpy.re')
}
return this.login({ username: 'e2e', password: process.env.PEERTUBE2_E2E_PASSWORD, url: 'https://peertube2.cpy.re/login' })
}
async logout () {
const loggedInDropdown = $('.logged-in-container .dropdown-toggle')
await loggedInDropdown.waitForClickable()
await loggedInDropdown.click()
const logout = $('.dropdown-item.logout')
await logout.waitForClickable()
await logout.click()
await browser.waitUntil(() => {
return $$('my-login-link, my-error-page a[href="/login"]').some(e => e.isDisplayed())
})
}
async ensureIsLoggedInAs (displayName: string) {
await this.getLoggedInInfoElem(displayName).waitForExist()
}
private getLoggedInInfoElem (displayName: string) {
return $('.logged-in-info').$('.display-name*=' + displayName)
}
private getSuffix () {
return browser.options.baseUrl
? browser.options.baseUrl.slice(-1)
: '1'
}
}

View File

@@ -0,0 +1,203 @@
import { NSFWPolicyType } from '@peertube/peertube-models'
import { getCheckbox, go, selectCustomSelect } from '../utils'
export class MyAccountPage {
navigateToMyVideos () {
return $('a[href="/my-library/videos"]').click()
}
navigateToMyPlaylists () {
return $('a[href="/my-library/video-playlists"]').click()
}
navigateToMyHistory () {
return $('a[href="/my-library/history/videos"]').click()
}
// ---------------------------------------------------------------------------
// My account settings
// ---------------------------------------------------------------------------
navigateToMySettings () {
return $('a[href="/my-account"]').click()
}
async updateNSFW (newValue: NSFWPolicyType) {
const nsfw = $(`#nsfwPolicy-${newValue} + label`)
await nsfw.waitForDisplayed()
await nsfw.scrollIntoView({ block: 'center' }) // Avoid issues with fixed header
await nsfw.waitForClickable()
await nsfw.click()
await this.submitVideoSettings()
}
async updateViolentFlag (newValue: NSFWPolicyType) {
const nsfw = $(`#nsfwFlagViolent-${newValue} + label`)
await nsfw.waitForDisplayed()
await nsfw.scrollIntoView({ block: 'center' }) // Avoid issues with fixed header
await nsfw.waitForClickable()
await nsfw.click()
await this.submitVideoSettings()
}
async clickOnP2PCheckbox () {
const p2p = await getCheckbox('p2pEnabled')
await p2p.waitForClickable()
await p2p.scrollIntoView({ block: 'center' }) // Avoid issues with fixed header
await p2p.click()
await this.submitVideoSettings()
}
private async submitVideoSettings () {
const submit = $('my-user-video-settings input[type=submit]')
await submit.waitForClickable()
await submit.scrollIntoView({ block: 'center' }) // Avoid issues with fixed header
await submit.click()
}
async updateEmail (email: string, password: string) {
const emailInput = $('my-account-change-email #new-email')
await emailInput.waitForDisplayed()
await emailInput.scrollIntoView({ block: 'center' }) // Avoid issues with fixed header
await emailInput.setValue(email)
const passwordInput = $('my-account-change-email #password')
await passwordInput.waitForDisplayed()
await passwordInput.setValue(password)
const submit = $('my-account-change-email input[type=submit]')
await submit.scrollIntoView({ block: 'center' }) // Avoid issues with fixed header
await submit.waitForClickable()
await submit.click()
}
// ---------------------------------------------------------------------------
// My account videos
// ---------------------------------------------------------------------------
async removeVideo (name: string) {
const container = await this.getVideoRow(name)
await container.$('my-action-dropdown .dropdown-toggle').click()
const deleteItem = () => {
return $$('.dropdown-menu .dropdown-item').find<WebdriverIO.Element>(async v => {
const text = await v.getText()
return text.includes('Delete')
})
}
await (await deleteItem()).waitForClickable()
return (await deleteItem()).click()
}
validRemove () {
return $('input[type=submit]').click()
}
async countVideos (names: string[]) {
const elements = await $$('.video-cell-name .name').filter(async e => {
const t = await e.getText()
return names.some(n => t.includes(n))
})
return elements.length
}
async getVideoRow (name: string) {
let el = $('.name*=' + name)
await el.waitForDisplayed()
while (await el.getTagName() !== 'tr') {
el = el.parentElement()
}
return el
}
// ---------------------------------------------------------------------------
// My account playlists
// ---------------------------------------------------------------------------
async getPlaylistVideosText (name: string) {
const elem = await this.getPlaylist(name)
return elem.$('.miniature-playlist-info-overlay').getText()
}
async clickOnPlaylist (name: string) {
const elem = await this.getPlaylist(name)
return elem.$('.miniature-thumbnail').click()
}
async countTotalPlaylistElements () {
await $('<my-video-playlist-element-miniature>').waitForDisplayed()
return $$('<my-video-playlist-element-miniature>').length
}
playPlaylist () {
return $('.playlist-info .miniature-thumbnail').click()
}
async goOnAssociatedPlaylistEmbed () {
let url = await browser.getUrl()
url = url.replace('/w/p/', '/video-playlists/embed/')
url = url.replace(':3333', ':9001')
return go(url)
}
async updatePlaylistPrivacy (playlistUUID: string, privacy: 'Public' | 'Private' | 'Unlisted') {
go('/my-library/video-playlists/update/' + playlistUUID)
await $('a[href*="/my-library/video-playlists/update/"]').waitForDisplayed()
await selectCustomSelect('videoChannelId', 'Main root channel')
await selectCustomSelect('privacy', privacy)
const submit = $('form input[type=submit]')
await submit.waitForClickable()
await submit.scrollIntoView()
await submit.click()
return browser.waitUntil(async () => {
return (await browser.getUrl()).includes('my-library/video-playlists')
})
}
private async getPlaylist (name: string) {
const playlist = () => {
return $$('my-video-playlist-miniature')
.filter(async e => {
const t = await e.$('img').getAttribute('aria-label')
return t.includes(name)
})
.then(elems => elems[0])
}
await browser.waitUntil(async () => {
const el = await playlist()
return el?.isDisplayed()
})
return playlist()
}
}

View File

@@ -0,0 +1,107 @@
import { browserSleep, isIOS, isMobileDevice, isSafari } from '../utils'
export class PlayerPage {
getWatchVideoPlayerCurrentTime () {
const elem = $('video')
const p = isIOS()
? elem.getAttribute('currentTime')
: elem.getProperty('currentTime')
return p.then(t => parseInt(t + '', 10))
.then(t => Math.ceil(t))
}
waitUntilPlaylistInfo (text: string, maxTime: number) {
return browser.waitUntil(async () => {
// Without this we have issues on iphone
await $('.video-js').click()
return (await $('.video-js .vjs-playlist-info').getText()).includes(text)
}, { timeout: maxTime })
}
waitUntilPlayerWrapper () {
return $('#video-wrapper').waitForExist()
}
waitUntilPlaying () {
return $('.video-js.vjs-playing').waitForDisplayed()
}
async playAndPauseVideo (isAutoplay: boolean, waitUntilSec: number) {
// Autoplay is disabled on mobile and Safari
if (isIOS() || isSafari() || isMobileDevice() || isAutoplay === false) {
await this.playVideo()
}
await $('div.video-js.vjs-has-started').waitForExist()
await browserSleep(2000)
await browser.waitUntil(async () => {
return (await this.getWatchVideoPlayerCurrentTime()) >= waitUntilSec
}, { timeout: Math.max(waitUntilSec * 2 * 1000, 30000) })
// Pause video
await $('div.video-js').click()
}
async playVideo () {
await $('div.video-js.vjs-paused, div.video-js.vjs-playing').waitForExist()
if (await $('div.video-js.vjs-playing').isExisting()) {
if (!isIOS()) return
// On iOS, the web browser may have aborted player autoplay, so check the video is still autoplayed
await browserSleep(5000)
if (await $('div.video-js.vjs-playing').isExisting()) return
}
// Autoplay is disabled on iOS and Safari
if (isIOS() || isSafari() || isMobileDevice()) {
// We can't play the video if it is not muted
await browser.execute(`document.querySelector('video').muted = true`)
}
return this.clickOnPlayButton()
}
getPlayButton () {
return $('.vjs-big-play-button')
}
getNSFWContentText () {
return $('.video-js .nsfw-info').getText()
}
getNSFWDetailsContent () {
return $('.video-js .nsfw-details-content')
}
getMoreNSFWInfoButton () {
return $('.video-js .nsfw-info button')
}
async hasPoster () {
const img = $('.video-js .vjs-poster img')
return await img.isDisplayed() && (await img.getAttribute('src')).startsWith('http')
}
private async clickOnPlayButton () {
await this.getPlayButton().waitForClickable()
await this.getPlayButton().click()
}
async fillEmbedVideoPassword (videoPassword: string) {
const videoPasswordInput = $('input#video-password-input')
const confirmButton = $('button#video-password-submit')
await videoPasswordInput.clearValue()
await videoPasswordInput.setValue(videoPassword)
await confirmButton.waitForClickable()
return confirmButton.click()
}
}

View File

@@ -0,0 +1,87 @@
import { getCheckbox } from '../utils'
export class SignupPage {
getRegisterMenuButton () {
return $('.create-account-button')
}
async clickOnRegisterButton () {
const button = this.getRegisterMenuButton()
await button.waitForClickable()
await button.click()
}
async validateStep () {
const next = $('button[type=submit]')
await next.scrollIntoView({ block: 'center' })
await next.waitForClickable()
await next.click()
}
async checkTerms () {
const terms = await getCheckbox('terms')
await terms.waitForClickable()
return terms.click()
}
async getEndMessage () {
const alert = $('.pt-alert-primary')
await alert.waitForDisplayed()
return alert.getText()
}
async fillRegistrationReason (reason: string) {
await $('#registrationReason').setValue(reason)
}
async fillAccountStep (options: {
username: string
password?: string
displayName?: string
email?: string
}) {
await $('#displayName').setValue(options.displayName || `${options.username} display name`)
await $('#username').setValue(options.username)
await $('#password').setValue(options.password || 'superpassword')
// Fix weird bug on firefox that "cannot scroll into view" when using just `setValue`
await $('#email').scrollIntoView({ block: 'center' })
await $('#email').waitForClickable()
await $('#email').setValue(options.email || `${options.username}@example.com`)
}
async fillChannelStep (options: {
name: string
displayName?: string
}) {
await $('#displayName').setValue(options.displayName || `${options.name} channel display name`)
await $('#name').setValue(options.name)
}
async fullSignup ({ accountInfo, channelInfo }: {
accountInfo: {
username: string
password?: string
displayName?: string
email?: string
}
channelInfo: {
name: string
}
}) {
await this.clickOnRegisterButton()
await this.validateStep()
await this.checkTerms()
await this.validateStep()
await this.fillAccountStep(accountInfo)
await this.validateStep()
await this.fillChannelStep(channelInfo)
await this.validateStep()
await this.getEndMessage()
}
}

View File

@@ -0,0 +1,140 @@
import { browserSleep, findParentElement, go } from '../utils'
export class VideoListPage {
constructor (private isMobileDevice: boolean, private isSafari: boolean) {
}
async goOnVideosList () {
let url: string
// We did not upload a file on a mobile device
if (this.isMobileDevice === true || this.isSafari === true) {
url = 'https://peertube2.cpy.re/videos/browse?scope=local'
} else {
url = '/videos/browse'
}
await go(url)
// Waiting the following element does not work on Safari...
if (this.isSafari) return browserSleep(3000)
await this.waitForList()
}
async goOnBrowseVideos () {
await $('.menu-link*=Home').click()
const browseVideos = $('a*=Browse videos')
await browseVideos.waitForClickable()
await browseVideos.click()
await this.waitForList()
}
async goOnHomepage () {
await go('/home')
await this.waitForList()
}
async goOnRootChannel () {
await go('/c/root_channel/videos')
await this.waitForList()
}
async goOnRootAccount () {
await go('/a/root/videos')
await this.waitForList()
}
async goOnRootAccountChannels () {
await go('/a/root/video-channels')
await this.waitForList()
}
async getNSFWFilterText () {
const el = $('.active-filter*=Sensitive')
await el.waitForDisplayed()
return el.getText()
}
async getVideosListName () {
const elems = $$('.videos .video-miniature .video-name')
const texts = await elems.map(e => e.getText())
return texts.map(t => t.trim())
}
isVideoDisplayed (name: string) {
return $('.video-name=' + name).isDisplayed()
}
async isVideoBlurred (name: string) {
const miniature = await this.getVideoMiniature(name)
const filter = await miniature.$('my-video-thumbnail img').getCSSProperty('filter')
return filter.value !== 'none'
}
async hasVideoWarning (name: string) {
const miniature = await this.getVideoMiniature(name)
return miniature.$('.nsfw-warning').isDisplayed()
}
async expectVideoNSFWTooltip (name: string, summary?: string) {
const miniature = await this.getVideoMiniature(name)
const warning = miniature.$('.nsfw-warning')
await warning.waitForDisplayed()
expect(await warning.getAttribute('aria-label')).toEqual(summary)
}
private async getVideoMiniature (name: string) {
const videoName = $('.video-name=' + name)
await videoName.waitForDisplayed()
return findParentElement(videoName, async el => await el.getTagName() === 'my-video-miniature')
}
async clickOnVideo (videoName: string) {
const video = async () => {
const videos = await $$('.videos .video-miniature .video-name').filter(async e => {
const t = await e.getText()
return t === videoName
})
return videos[0]
}
await browser.waitUntil(async () => {
const elem = await video()
return elem?.isClickable()
})
;(await video()).click()
await browser.waitUntil(async () => (await browser.getUrl()).includes('/w/'))
}
async clickOnFirstVideo () {
const video = () => $('.videos .video-miniature .video-thumbnail')
const videoName = () => $('.videos .video-miniature .video-name')
await video().waitForClickable()
const textToReturn = await videoName().getText()
await video().click()
await browser.waitUntil(async () => (await browser.getUrl()).includes('/w/'))
return textToReturn
}
private waitForList () {
return $('.videos .video-miniature .video-name').waitForDisplayed()
}
}

View File

@@ -0,0 +1,148 @@
import { clickOnRadio, getCheckbox, go, isRadioSelected, selectCustomSelect, setCheckboxEnabled } from '../utils'
export abstract class VideoManage {
async clickOnSave () {
const button = this.getSaveButton()
await button.waitForClickable()
await button.click()
await this.waitForSaved()
}
async clickOnWatch () {
// Simulate the click, because the button opens a new tab
const button = $('.watch-save > my-button[icon=external-link] a')
await button.waitForClickable()
await go(await button.getAttribute('href'))
}
// ---------------------------------------------------------------------------
async setAsNSFW (options: {
violent?: boolean
summary?: string
} = {}) {
await this.goOnPage('Moderation')
const checkbox = await getCheckbox('nsfw')
await checkbox.waitForClickable()
await checkbox.click()
if (options.violent) {
await setCheckboxEnabled('nsfwFlagViolent', true)
}
if (options.summary) {
await $('#nsfwSummary').setValue(options.summary)
}
}
// ---------------------------------------------------------------------------
async setAsPublic () {
await this.goOnPage('Main information')
return selectCustomSelect('privacy', 'Public')
}
async setAsPrivate () {
await this.goOnPage('Main information')
return selectCustomSelect('privacy', 'Private')
}
async setAsPasswordProtected (videoPassword: string) {
await this.goOnPage('Main information')
selectCustomSelect('privacy', 'Password protected')
const videoPasswordInput = $('input#videoPassword')
await videoPasswordInput.waitForClickable()
await videoPasswordInput.clearValue()
return videoPasswordInput.setValue(videoPassword)
}
// ---------------------------------------------------------------------------
async scheduleUpload () {
await this.goOnPage('Main information')
selectCustomSelect('privacy', 'Scheduled')
const input = this.getScheduleInput()
await input.waitForClickable()
await input.click()
const nextMonth = $('.p-datepicker-next-button')
await nextMonth.click()
await $('.p-datepicker-calendar td[aria-label="1"] > span').click()
await $('.p-datepicker-calendar').waitForDisplayed({ reverse: true, timeout: 15000 }) // Can be slow
}
getScheduleInput () {
return $('#schedulePublicationAt input')
}
// ---------------------------------------------------------------------------
async setNormalLive () {
await this.goOnPage('Live settings')
await clickOnRadio('permanentLiveFalse')
}
async setPermanentLive () {
await this.goOnPage('Live settings')
await clickOnRadio('permanentLiveTrue')
}
async getLiveState () {
await this.goOnPage('Live settings')
if (await isRadioSelected('permanentLiveTrue')) return 'permanent'
return 'normal'
}
// ---------------------------------------------------------------------------
async refresh (videoName: string) {
await browser.refresh()
await browser.waitUntil(async () => {
const url = await browser.getUrl()
return url.includes('/videos/manage')
})
await browser.waitUntil(async () => {
return await $('#name').getValue() === videoName
})
}
// ---------------------------------------------------------------------------
protected getSaveButton () {
return $('.save-button > button:not([disabled])')
}
protected waitForSaved () {
return $('.save-button > button[disabled], my-manage-errors').waitForDisplayed()
}
protected async goOnPage (page: 'Main information' | 'Moderation' | 'Live settings') {
const urls = {
'Main information': '',
'Moderation': 'moderation',
'Live settings': 'live'
}
const el = $(`my-video-manage-container .menu a[href*="/${urls[page]}"]`)
await el.waitForClickable()
await el.click()
}
}

View File

@@ -0,0 +1,80 @@
import { join } from 'node:path'
import { VideoManage } from './video-manage'
import { FIXTURE_URLS } from '../utils'
export class VideoPublishPage extends VideoManage {
async navigateTo (tab?: 'Go live') {
const publishButton = $('.publish-button > a')
await publishButton.waitForClickable()
await publishButton.click()
await $('.upload-video-container').waitForDisplayed()
if (tab) {
const el = $(`.nav-link*=${tab}`)
await el.waitForClickable()
await el.click()
}
}
// ---------------------------------------------------------------------------
async uploadVideo (fixtureName: 'video.mp4' | 'video2.mp4' | 'video3.mp4') {
const fileToUpload = join(__dirname, '../../fixtures/' + fixtureName)
const fileInputSelector = '.upload-video-container input[type=file]'
const parentFileInput = '.upload-video-container .button-file'
// Avoid sending keys on non visible element
await browser.execute(`document.querySelector('${fileInputSelector}').style.opacity = 1`)
await browser.execute(`document.querySelector('${parentFileInput}').style.overflow = 'initial'`)
await browser.pause(1000)
const elem = $(fileInputSelector)
await elem.chooseFile(fileToUpload)
// Wait for the upload to finish
await this.getSaveButton().waitForClickable()
}
async importVideo () {
const tab = $('.nav-link*=Import with URL')
await tab.waitForClickable()
await tab.click()
const input = $('#targetUrl')
await input.waitForDisplayed()
await input.setValue(FIXTURE_URLS.IMPORT_URL)
const submit = $('.first-step-block .primary-button:not([disabled])')
await submit.waitForClickable()
await submit.click()
// Wait for the import to finish
await this.getSaveButton().waitForClickable({ timeout: 15000 }) // Can be slow
}
async publishLive () {
await $('#permanentLiveTrue').parentElement().click()
const submit = $('.upload-video-container .primary-button:not([disabled])')
await submit.waitForClickable()
await submit.click()
await this.getSaveButton().waitForClickable()
}
// ---------------------------------------------------------------------------
async validSecondStep (videoName: string) {
await this.goOnPage('Main information')
const nameInput = $('input#name')
await nameInput.scrollIntoView()
await nameInput.clearValue()
await nameInput.setValue(videoName)
await this.clickOnSave()
}
}

View File

@@ -0,0 +1,11 @@
export class VideoSearchPage {
async search (search: string) {
await $('#search-video').setValue(search)
await $('.search-button').click()
await browser.waitUntil(() => {
return $('my-video-miniature').isDisplayed()
})
}
}

View File

@@ -0,0 +1,11 @@
import { VideoManage } from './video-manage'
export class VideoUpdatePage extends VideoManage {
async updateName (videoName: string) {
const nameInput = $('input#name')
await nameInput.waitForDisplayed()
await nameInput.clearValue()
await nameInput.setValue(videoName)
}
}

View File

@@ -0,0 +1,229 @@
import { browserSleep, FIXTURE_URLS, go } from '../utils'
export class VideoWatchPage {
constructor (private isMobileDevice: boolean, private isSafari: boolean) {
}
waitWatchVideoName (videoName: string, maxTime?: number) {
if (this.isSafari) return browserSleep(5000)
// On mobile we display the first node, on desktop the second one
return browser.waitUntil(async () => {
return (await this.getVideoName()) === videoName
}, { timeout: maxTime })
}
getVideoName () {
return this.getVideoNameElement().then(e => e.getText())
}
getPrivacy () {
return $('.attribute-privacy .attribute-value').getText()
}
getLicence () {
return $('.attribute-licence .attribute-value').getText()
}
async isDownloadEnabled () {
try {
await this.clickOnMoreDropdownIcon()
return await $('.dropdown-item .icon-download').isExisting()
} catch {
return $('.action-button-download').isDisplayed()
}
}
areCommentsEnabled () {
return $('my-video-comment-add').isExisting()
}
isPrivacyWarningDisplayed () {
return $('.privacy-concerns-text').isDisplayed()
}
async goOnAssociatedEmbed (passwordProtected = false) {
let url = await browser.getUrl()
url = url.replace('/w/', '/videos/embed/')
url = url.replace(':3333', ':9001')
await go(url)
if (passwordProtected) await this.waitEmbedForVideoPasswordForm()
else await this.waitEmbedForDisplayed()
}
waitEmbedForDisplayed () {
return $('.vjs-big-play-button').waitForDisplayed()
}
waitEmbedForVideoPasswordForm () {
return $('#video-password-input').waitForDisplayed()
}
isEmbedWarningDisplayed () {
return $('.peertube-dock-description').isDisplayed()
}
goOnP2PMediaLoaderEmbed () {
return go(FIXTURE_URLS.HLS_EMBED)
}
goOnP2PMediaLoaderPlaylistEmbed () {
return go(FIXTURE_URLS.HLS_PLAYLIST_EMBED)
}
getModalTitleEl () {
return $('.modal-content .modal-title')
}
confirmModal () {
return $('.modal-content .modal-footer .primary-button').click()
}
private getVideoNameElement () {
return $('.video-info-first-row .video-info-name')
}
// ---------------------------------------------------------------------------
// Video password
// ---------------------------------------------------------------------------
async fillVideoPassword (videoPassword: string) {
const videoPasswordInput = $('input#confirmInput')
await videoPasswordInput.waitForClickable()
await videoPasswordInput.clearValue()
await videoPasswordInput.setValue(videoPassword)
const confirmButton = $('input[value="Confirm"]')
await confirmButton.waitForClickable()
return confirmButton.click()
}
// ---------------------------------------------------------------------------
// Video actions
// ---------------------------------------------------------------------------
async like () {
const likeButton = $('.action-button-like')
const isActivated = (await likeButton.getAttribute('class')).includes('activated')
let count: number
try {
count = parseInt(await $('.action-button-like > .count').getText())
} catch (error) {
count = 0
}
await likeButton.waitForClickable()
await likeButton.click()
if (isActivated) {
if (count === 1) {
return expect(!await $('.action-button-like > .count').isExisting())
} else {
return expect(parseInt(await $('.action-button-like > .count').getText())).toBe(count - 1)
}
} else {
return expect(parseInt(await $('.action-button-like > .count').getText())).toBe(count + 1)
}
}
async clickOnManage () {
await this.clickOnMoreDropdownIcon()
// We need the await expression
return $$('.dropdown-menu.show .dropdown-item').forEach(async item => {
const content = await item.getText()
if (content.includes('Manage')) {
await item.click()
await $('#name').waitForClickable()
return
}
})
}
async clickOnMoreDropdownIcon () {
const dropdown = $('my-video-actions-dropdown .action-button')
await dropdown.scrollIntoView({ block: 'center' })
await dropdown.click()
await $('.dropdown-menu.show .dropdown-item').waitForDisplayed()
}
// ---------------------------------------------------------------------------
// Playlists
// ---------------------------------------------------------------------------
async clickOnSave () {
const button = $('.action-button-save')
await button.scrollIntoView({ block: 'center' })
return button.click()
}
async createPlaylist (name: string) {
const newPlaylistButton = () => $('.new-playlist-button')
await newPlaylistButton().waitForClickable()
await newPlaylistButton().click()
const displayName = () => $('#displayName')
await displayName().waitForDisplayed()
await displayName().setValue(name)
return $('.new-playlist-block input[type=submit]').click()
}
async saveToPlaylist (name: string) {
const playlist = () => $('my-video-add-to-playlist').$(`.playlist=${name}`)
await playlist().waitForDisplayed()
return playlist().click()
}
// ---------------------------------------------------------------------------
// Comments
// ---------------------------------------------------------------------------
async createThread (comment: string) {
const textarea = $('my-video-comment-add textarea')
await textarea.waitForClickable()
await textarea.setValue(comment)
const confirmButton = $('.comment-buttons .primary-button')
await confirmButton.waitForClickable()
await confirmButton.click()
const createdComment = await $('.comment-html p').getText()
return expect(createdComment).toBe(comment)
}
async createReply (comment: string) {
const replyButton = $('button.comment-action-reply')
await replyButton.waitForClickable()
await replyButton.scrollIntoView({ block: 'center' })
await replyButton.click()
const textarea = $('my-video-comment my-video-comment-add textarea')
await textarea.waitForClickable()
await textarea.setValue(comment)
const confirmButton = $('my-video-comment .comment-buttons .primary-button')
await confirmButton.waitForClickable()
await replyButton.scrollIntoView({ block: 'center' })
await confirmButton.click()
const createdComment = $('.is-child .comment-html p')
await createdComment.waitForDisplayed()
return expect(await createdComment.getText()).toBe(comment)
}
}

View File

@@ -0,0 +1,33 @@
import { PlayerPage } from '../po/player.po'
import { VideoWatchPage } from '../po/video-watch.po'
import { FIXTURE_URLS, go, isMobileDevice, isSafari, prepareWebBrowser } from '../utils'
describe('Live all workflow', () => {
let videoWatchPage: VideoWatchPage
let playerPage: PlayerPage
beforeEach(async () => {
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
playerPage = new PlayerPage()
await prepareWebBrowser()
})
it('Should go to the live page', async () => {
await go(FIXTURE_URLS.LIVE_VIDEO)
return videoWatchPage.waitWatchVideoName('E2E - Live')
})
it('Should play the live', async () => {
await playerPage.playAndPauseVideo(false, 45)
expect(await playerPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(45)
})
it('Should watch the associated live embed', async () => {
await videoWatchPage.goOnAssociatedEmbed()
await playerPage.playAndPauseVideo(false, 45)
expect(await playerPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(45)
})
})

View File

@@ -0,0 +1,73 @@
import { LoginPage } from '../po/login.po'
import { PlayerPage } from '../po/player.po'
import { VideoWatchPage } from '../po/video-watch.po'
import { FIXTURE_URLS, go, isMobileDevice, isSafari, prepareWebBrowser } from '../utils'
async function checkCorrectlyPlay (playerPage: PlayerPage) {
await playerPage.playAndPauseVideo(false, 2)
expect(await playerPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2)
}
describe('Private videos all workflow', () => {
let videoWatchPage: VideoWatchPage
let loginPage: LoginPage
let playerPage: PlayerPage
const internalVideoName = 'Internal E2E test'
const internalHLSOnlyVideoName = 'Internal E2E test - HLS only'
beforeEach(async () => {
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
loginPage = new LoginPage(isMobileDevice())
playerPage = new PlayerPage()
await prepareWebBrowser()
})
it('Should log in', async () => {
return loginPage.loginOnPeerTube2()
})
it('Should play an internal web video', async () => {
await go(FIXTURE_URLS.INTERNAL_WEB_VIDEO)
await videoWatchPage.waitWatchVideoName(internalVideoName)
await checkCorrectlyPlay(playerPage)
})
it('Should play an internal HLS video', async () => {
await go(FIXTURE_URLS.INTERNAL_HLS_VIDEO)
await videoWatchPage.waitWatchVideoName(internalVideoName)
await checkCorrectlyPlay(playerPage)
})
it('Should play an internal HLS only video', async () => {
await go(FIXTURE_URLS.INTERNAL_HLS_ONLY_VIDEO)
await videoWatchPage.waitWatchVideoName(internalHLSOnlyVideoName)
await checkCorrectlyPlay(playerPage)
})
it('Should play an internal Web Video in embed', async () => {
await go(FIXTURE_URLS.INTERNAL_EMBED_WEB_VIDEO)
await videoWatchPage.waitEmbedForDisplayed()
await checkCorrectlyPlay(playerPage)
})
it('Should play an internal HLS video in embed', async () => {
await go(FIXTURE_URLS.INTERNAL_EMBED_HLS_VIDEO)
await videoWatchPage.waitEmbedForDisplayed()
await checkCorrectlyPlay(playerPage)
})
it('Should play an internal HLS only video in embed', async () => {
await go(FIXTURE_URLS.INTERNAL_EMBED_HLS_ONLY_VIDEO)
await videoWatchPage.waitEmbedForDisplayed()
await checkCorrectlyPlay(playerPage)
})
})

View File

@@ -0,0 +1,233 @@
import { LoginPage } from '../po/login.po'
import { MyAccountPage } from '../po/my-account.po'
import { PlayerPage } from '../po/player.po'
import { VideoListPage } from '../po/video-list.po'
import { VideoPublishPage } from '../po/video-publish.po'
import { VideoUpdatePage } from '../po/video-update.po'
import { VideoWatchPage } from '../po/video-watch.po'
import { FIXTURE_URLS, go, isIOS, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
function isUploadUnsupported () {
if (isMobileDevice() || isSafari()) {
console.log('Skipping because we are on a real device or Safari and BrowserStack does not support file upload.')
return true
}
return false
}
describe('Videos all workflow', () => {
let videoWatchPage: VideoWatchPage
let videoListPage: VideoListPage
let videoPublishPage: VideoPublishPage
let videoUpdatePage: VideoUpdatePage
let myAccountPage: MyAccountPage
let loginPage: LoginPage
let playerPage: PlayerPage
let videoName = Math.random() + ' video'
const video2Name = Math.random() + ' second video'
const playlistName = Math.random() + ' playlist'
let videoWatchUrl: string
before(async () => {
if (isIOS()) {
console.log('iOS detected')
} else if (isMobileDevice()) {
console.log('Android detected.')
} else if (isSafari()) {
console.log('Safari detected.')
}
if (isUploadUnsupported()) return
await waitServerUp()
})
beforeEach(async () => {
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
videoPublishPage = new VideoPublishPage()
videoUpdatePage = new VideoUpdatePage()
myAccountPage = new MyAccountPage()
loginPage = new LoginPage(isMobileDevice())
playerPage = new PlayerPage()
videoListPage = new VideoListPage(isMobileDevice(), isSafari())
await prepareWebBrowser()
})
it('Should log in', async () => {
if (isMobileDevice() || isSafari()) {
console.log('Skipping because we are on a real device or Safari and BrowserStack does not support file upload.')
return
}
return loginPage.loginAsRootUser()
})
it('Should upload a video', async () => {
if (isUploadUnsupported()) return
await videoPublishPage.navigateTo()
await videoPublishPage.uploadVideo('video.mp4')
await videoPublishPage.validSecondStep(videoName)
})
it('Should list videos', async () => {
await videoListPage.goOnVideosList()
if (isUploadUnsupported()) return
const videoNames = await videoListPage.getVideosListName()
expect(videoNames).toContain(videoName)
})
it('Should go on video watch page', async () => {
let videoNameToExcept = videoName
if (isMobileDevice() || isSafari()) {
await go(FIXTURE_URLS.WEB_VIDEO)
videoNameToExcept = 'E2E tests'
} else {
await videoListPage.clickOnVideo(videoName)
}
return videoWatchPage.waitWatchVideoName(videoNameToExcept)
})
it('Should play the video', async () => {
videoWatchUrl = await browser.getUrl()
await playerPage.playAndPauseVideo(true, 2)
expect(await playerPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2)
})
it('Should watch the associated embed video', async () => {
await videoWatchPage.goOnAssociatedEmbed()
await playerPage.playAndPauseVideo(false, 2)
expect(await playerPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2)
})
it('Should watch the p2p media loader embed video', async () => {
await videoWatchPage.goOnP2PMediaLoaderEmbed()
await playerPage.playAndPauseVideo(false, 2)
expect(await playerPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2)
})
it('Should update the video', async () => {
if (isUploadUnsupported()) return
await go(videoWatchUrl)
await videoWatchPage.clickOnManage()
videoName += ' updated'
await videoUpdatePage.updateName(videoName)
await videoUpdatePage.clickOnSave()
await videoUpdatePage.clickOnWatch()
const name = await videoWatchPage.getVideoName()
expect(name).toEqual(videoName)
})
it('Should add the video in my playlist', async () => {
if (isUploadUnsupported()) return
await videoWatchPage.clickOnSave()
await videoWatchPage.createPlaylist(playlistName)
await videoWatchPage.saveToPlaylist(playlistName)
await browser.pause(5000)
await videoPublishPage.navigateTo()
await videoPublishPage.uploadVideo('video2.mp4')
await videoPublishPage.validSecondStep(video2Name)
await videoPublishPage.clickOnWatch()
await videoWatchPage.clickOnSave()
await videoWatchPage.saveToPlaylist(playlistName)
})
it('Should have the playlist in my account', async () => {
if (isUploadUnsupported()) return
await myAccountPage.navigateToMyPlaylists()
const videosNumberText = await myAccountPage.getPlaylistVideosText(playlistName)
expect(videosNumberText).toEqual('2 videos')
await myAccountPage.clickOnPlaylist(playlistName)
const count = await myAccountPage.countTotalPlaylistElements()
expect(count).toEqual(2)
})
it('Should watch the playlist', async () => {
if (isUploadUnsupported()) return
await myAccountPage.playPlaylist()
await videoWatchPage.waitWatchVideoName(video2Name, 40 * 1000)
})
it('Should watch the Web Video playlist in the embed', async () => {
if (isUploadUnsupported()) return
const accessToken = await browser.execute(`return window.localStorage.getItem('access_token');`)
const refreshToken = await browser.execute(`return window.localStorage.getItem('refresh_token');`)
await myAccountPage.goOnAssociatedPlaylistEmbed()
await playerPage.waitUntilPlayerWrapper()
console.log('Will set %s and %s tokens in local storage.', accessToken, refreshToken)
await browser.execute(`window.localStorage.setItem('access_token', '${accessToken}');`)
await browser.execute(`window.localStorage.setItem('refresh_token', '${refreshToken}');`)
await browser.execute(`window.localStorage.setItem('token_type', 'Bearer');`)
await browser.refresh()
await playerPage.playVideo()
await playerPage.waitUntilPlaylistInfo('2/2', 30 * 1000)
})
it('Should watch the HLS playlist in the embed', async () => {
await videoWatchPage.goOnP2PMediaLoaderPlaylistEmbed()
await playerPage.playVideo()
await playerPage.waitUntilPlaylistInfo('2/2', 30 * 1000)
})
it('Should delete the video 2', async () => {
if (isUploadUnsupported()) return
// Go to the dev website
await go(videoWatchUrl)
await myAccountPage.navigateToMyVideos()
await myAccountPage.removeVideo(video2Name)
await myAccountPage.validRemove()
await browser.waitUntil(async () => {
const count = await myAccountPage.countVideos([ videoName, video2Name ])
return count === 1
})
})
it('Should delete the first video', async () => {
if (isUploadUnsupported()) return
await myAccountPage.removeVideo(videoName)
await myAccountPage.validRemove()
})
})

View File

@@ -0,0 +1,95 @@
import { LoginPage } from '../po/login.po'
import { VideoPublishPage } from '../po/video-publish.po'
import { VideoWatchPage } from '../po/video-watch.po'
import { go, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
describe('Custom server defaults', () => {
let videoPublishPage: VideoPublishPage
let loginPage: LoginPage
let videoWatchPage: VideoWatchPage
before(async () => {
await waitServerUp()
loginPage = new LoginPage(isMobileDevice())
videoPublishPage = new VideoPublishPage()
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
await prepareWebBrowser({ hidePrivacyConcerns: false })
})
describe('Publish default values', function () {
before(async function () {
await loginPage.loginAsRootUser()
})
it('Should upload a video with custom default values', async function () {
await videoPublishPage.navigateTo()
await videoPublishPage.uploadVideo('video.mp4')
await videoPublishPage.validSecondStep('video')
await videoPublishPage.clickOnWatch()
await videoWatchPage.waitWatchVideoName('video')
const videoUrl = await browser.getUrl()
expect(await videoWatchPage.getPrivacy()).toBe('Unlisted')
expect(await videoWatchPage.getLicence()).toBe('Attribution - Non Commercial')
expect(await videoWatchPage.areCommentsEnabled()).toBeFalsy()
// Owners can download their videos
expect(await videoWatchPage.isDownloadEnabled()).toBeTruthy()
// Logout to see if the download enabled is correct for anonymous users
await loginPage.logout()
await browser.url(videoUrl)
await videoWatchPage.waitWatchVideoName('video')
expect(await videoWatchPage.isDownloadEnabled()).toBeFalsy()
})
})
describe('P2P', function () {
let videoUrl: string
async function goOnVideoWatchPage () {
await go(videoUrl)
await videoWatchPage.waitWatchVideoName('video')
}
async function checkP2P (enabled: boolean) {
await goOnVideoWatchPage()
expect(await videoWatchPage.isPrivacyWarningDisplayed()).toEqual(enabled)
await videoWatchPage.goOnAssociatedEmbed()
expect(await videoWatchPage.isEmbedWarningDisplayed()).toEqual(enabled)
}
before(async () => {
await loginPage.loginAsRootUser()
await videoPublishPage.navigateTo()
await videoPublishPage.uploadVideo('video2.mp4')
await videoPublishPage.setAsPublic()
await videoPublishPage.validSecondStep('video')
await videoPublishPage.clickOnWatch()
await videoWatchPage.waitWatchVideoName('video')
videoUrl = await browser.getUrl()
})
beforeEach(async function () {
await goOnVideoWatchPage()
})
it('Should have P2P disabled for a logged in user', async function () {
await checkP2P(false)
})
it('Should have P2P disabled for anonymous users', async function () {
await loginPage.logout()
await checkP2P(false)
})
})
})

View File

@@ -0,0 +1,393 @@
import { NSFWPolicyType } from '@peertube/peertube-models'
import { AdminConfigPage } from '../po/admin-config.po'
import { AdminUserPage } from '../po/admin-user.po'
import { AnonymousSettingsPage } from '../po/anonymous-settings.po'
import { LoginPage } from '../po/login.po'
import { MyAccountPage } from '../po/my-account.po'
import { PlayerPage } from '../po/player.po'
import { VideoListPage } from '../po/video-list.po'
import { VideoPublishPage } from '../po/video-publish.po'
import { VideoSearchPage } from '../po/video-search.po'
import { VideoWatchPage } from '../po/video-watch.po'
import { getScreenshotPath, go, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
describe('NSFW', () => {
let videoListPage: VideoListPage
let videoPublishPage: VideoPublishPage
let adminConfigPage: AdminConfigPage
let loginPage: LoginPage
let adminUserPage: AdminUserPage
let myAccountPage: MyAccountPage
let videoSearchPage: VideoSearchPage
let videoWatchPage: VideoWatchPage
let playerPage: PlayerPage
let anonymousSettingsPage: AnonymousSettingsPage
const seed = Math.random()
const nsfwVideo = seed + ' - nsfw'
const violentVideo = seed + ' - violent'
const normalVideo = seed + ' - normal'
let videoUrl: string
async function checkVideo (options: {
policy: NSFWPolicyType
videoName: string
nsfwTooltip?: string
}) {
const { policy, videoName, nsfwTooltip } = options
if (policy === 'do_not_list') {
expect(await videoListPage.isVideoDisplayed(videoName)).toBeFalsy()
} else if (policy === 'warn') {
expect(await videoListPage.isVideoDisplayed(videoName)).toBeTruthy()
expect(await videoListPage.isVideoBlurred(videoName)).toBeFalsy()
expect(await videoListPage.hasVideoWarning(videoName)).toBeTruthy()
} else if (policy === 'blur') {
expect(await videoListPage.isVideoDisplayed(videoName)).toBeTruthy()
expect(await videoListPage.isVideoBlurred(videoName)).toBeTruthy()
expect(await videoListPage.hasVideoWarning(videoName)).toBeTruthy()
} else { // Display
expect(await videoListPage.isVideoDisplayed(videoName)).toBeTruthy()
expect(await videoListPage.isVideoBlurred(videoName)).toBeFalsy()
expect(await videoListPage.hasVideoWarning(videoName)).toBeFalsy()
}
if (nsfwTooltip) {
await videoListPage.expectVideoNSFWTooltip(videoName, nsfwTooltip)
}
}
async function checkFilterText (policy: NSFWPolicyType) {
const pagesWithFilters = [
videoListPage.goOnRootAccount.bind(videoListPage),
videoListPage.goOnBrowseVideos.bind(videoListPage),
videoListPage.goOnRootChannel.bind(videoListPage)
]
for (const goOnPage of pagesWithFilters) {
await goOnPage()
const filterText = await videoListPage.getNSFWFilterText()
if (policy === 'do_not_list') {
expect(filterText).toContain('hidden')
} else if (policy === 'warn') {
expect(filterText).toContain('warned')
} else if (policy === 'blur') {
expect(filterText).toContain('blurred')
} else {
expect(filterText).toContain('displayed')
}
}
}
async function checkCommonVideoListPages (policy: NSFWPolicyType, videos: string[], nsfwTooltip?: string) {
const pages = [
videoListPage.goOnRootAccount.bind(videoListPage),
videoListPage.goOnBrowseVideos.bind(videoListPage),
videoListPage.goOnRootChannel.bind(videoListPage),
videoListPage.goOnRootAccountChannels.bind(videoListPage),
videoListPage.goOnHomepage.bind(videoListPage)
]
for (const goOnPage of pages) {
await goOnPage()
for (const video of videos) {
await browser.saveScreenshot(getScreenshotPath('before-nsfw-test.png'))
await checkVideo({ policy, videoName: video, nsfwTooltip })
}
}
for (const video of videos) {
await videoSearchPage.search(video)
await browser.saveScreenshot(getScreenshotPath('before-nsfw-test.png'))
await checkVideo({ policy, videoName: video, nsfwTooltip })
}
}
async function updateAdminNSFW (nsfw: NSFWPolicyType) {
await adminConfigPage.updateNSFWSetting(nsfw)
await adminConfigPage.save()
}
async function updateUserNSFW (nsfw: NSFWPolicyType, loggedIn: boolean) {
if (loggedIn) {
await myAccountPage.navigateToMySettings()
await myAccountPage.updateNSFW(nsfw)
return
}
await anonymousSettingsPage.openSettings()
await anonymousSettingsPage.updateNSFW(nsfw)
await anonymousSettingsPage.closeSettings()
}
async function updateUserViolentNSFW (nsfw: NSFWPolicyType, loggedIn: boolean) {
if (loggedIn) {
await myAccountPage.navigateToMySettings()
await myAccountPage.updateViolentFlag(nsfw)
return
}
await anonymousSettingsPage.openSettings()
await anonymousSettingsPage.updateViolentFlag(nsfw)
await anonymousSettingsPage.closeSettings()
}
before(async () => {
await waitServerUp()
})
beforeEach(async () => {
videoListPage = new VideoListPage(isMobileDevice(), isSafari())
adminConfigPage = new AdminConfigPage()
loginPage = new LoginPage(isMobileDevice())
adminUserPage = new AdminUserPage()
videoPublishPage = new VideoPublishPage()
myAccountPage = new MyAccountPage()
videoSearchPage = new VideoSearchPage()
playerPage = new PlayerPage()
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
anonymousSettingsPage = new AnonymousSettingsPage()
await prepareWebBrowser()
})
describe('Preparation', function () {
it('Should login and disable NSFW', async () => {
await loginPage.loginAsRootUser()
await updateUserNSFW('display', true)
})
it('Should set the homepage', async () => {
await adminConfigPage.updateHomepage('<peertube-videos-list data-sort="-publishedAt"></peertube-videos-list>')
await adminConfigPage.save()
})
it('Should create a user', async () => {
await adminUserPage.createUser({
username: 'user_' + seed,
password: 'superpassword'
})
})
it('Should upload NSFW and normal videos', async () => {
await videoPublishPage.navigateTo()
await videoPublishPage.uploadVideo('video.mp4')
await videoPublishPage.setAsNSFW()
await videoPublishPage.validSecondStep(nsfwVideo)
await videoPublishPage.navigateTo()
await videoPublishPage.uploadVideo('video.mp4')
await videoPublishPage.setAsNSFW({ summary: 'bibi is violent', violent: true })
await videoPublishPage.validSecondStep(violentVideo)
await videoPublishPage.navigateTo()
await videoPublishPage.uploadVideo('video2.mp4')
await videoPublishPage.validSecondStep(normalVideo)
})
it('Should logout', async function () {
await loginPage.logout()
})
})
describe('NSFW with an anonymous users using instance default', function () {
it('Should correctly handle do not list', async () => {
await loginPage.loginAsRootUser()
await updateAdminNSFW('do_not_list')
await loginPage.logout()
await checkCommonVideoListPages('do_not_list', [ nsfwVideo, violentVideo ])
await checkCommonVideoListPages('display', [ normalVideo ])
await checkFilterText('do_not_list')
})
it('Should correctly handle blur', async () => {
await loginPage.loginAsRootUser()
await updateAdminNSFW('blur')
await loginPage.logout()
await checkCommonVideoListPages('blur', [ nsfwVideo, violentVideo ])
await checkCommonVideoListPages('display', [ normalVideo ])
await checkFilterText('blur')
})
it('Should not autoplay the video and display a warning on watch/embed page', async function () {
await videoListPage.clickOnVideo(nsfwVideo)
await videoWatchPage.waitWatchVideoName(nsfwVideo)
videoUrl = await browser.getUrl()
const check = async () => {
expect(await playerPage.getPlayButton().isDisplayed()).toBeTruthy()
expect(await playerPage.getNSFWContentText()).toContain('This video contains sensitive content')
expect(await playerPage.getMoreNSFWInfoButton().isDisplayed()).toBeFalsy()
expect(await playerPage.hasPoster()).toBeFalsy()
}
await check()
await videoWatchPage.goOnAssociatedEmbed()
await check()
})
it('Should correctly handle warn', async () => {
await loginPage.loginAsRootUser()
await updateAdminNSFW('warn')
await loginPage.logout()
await checkCommonVideoListPages('warn', [ nsfwVideo, violentVideo ])
await checkCommonVideoListPages('display', [ normalVideo ])
await checkFilterText('warn')
})
it('Should not autoplay the video and display a warning on watch/embed page', async function () {
await videoListPage.clickOnVideo(violentVideo)
await videoWatchPage.waitWatchVideoName(violentVideo)
const check = async () => {
expect(await playerPage.getPlayButton().isDisplayed()).toBeTruthy()
expect(await playerPage.getNSFWContentText()).toContain('This video contains sensitive content')
expect(await playerPage.hasPoster()).toBeTruthy()
const moreButton = playerPage.getMoreNSFWInfoButton()
expect(await moreButton.isDisplayed()).toBeTruthy()
await moreButton.click()
await playerPage.getNSFWDetailsContent().waitForDisplayed()
const moreContent = await playerPage.getNSFWDetailsContent().getText()
expect(moreContent).toContain('Potentially violent content')
expect(moreContent).toContain('bibi is violent')
}
await check()
await videoWatchPage.goOnAssociatedEmbed()
await check()
})
it('Should correctly handle display', async () => {
await loginPage.loginAsRootUser()
await updateAdminNSFW('display')
await loginPage.logout()
await checkCommonVideoListPages('display', [ nsfwVideo, violentVideo, normalVideo ])
await checkFilterText('display')
})
it('Should autoplay the video on watch page', async function () {
await videoListPage.clickOnVideo(nsfwVideo)
await videoWatchPage.waitWatchVideoName(nsfwVideo)
expect(await playerPage.getPlayButton().isDisplayed()).toBeFalsy()
})
})
describe('NSFW settings', function () {
function runSuite (loggedIn: boolean) {
it('Should correctly handle do not list', async () => {
await updateUserNSFW('do_not_list', loggedIn)
await checkCommonVideoListPages('do_not_list', [ nsfwVideo, violentVideo ])
await checkCommonVideoListPages('display', [ normalVideo ])
await checkFilterText('do_not_list')
})
it('Should use a confirm modal when viewing the video and watch the video', async function () {
await go(videoUrl)
const confirmTitle = videoWatchPage.getModalTitleEl()
await confirmTitle.waitForDisplayed()
expect(await confirmTitle.getText()).toContain('Sensitive video')
await videoWatchPage.confirmModal()
await videoWatchPage.waitWatchVideoName(nsfwVideo)
})
it('Should correctly handle blur', async () => {
await updateUserNSFW('blur', loggedIn)
await checkCommonVideoListPages('blur', [ nsfwVideo ], 'This video contains sensitive content')
await checkCommonVideoListPages('blur', [ violentVideo ], 'This video contains sensitive content: violence')
await checkCommonVideoListPages('display', [ normalVideo ])
await checkFilterText('blur')
})
it('Should correctly handle warn', async () => {
await updateUserNSFW('warn', loggedIn)
await checkCommonVideoListPages('warn', [ nsfwVideo ], 'This video contains sensitive content')
await checkCommonVideoListPages('warn', [ violentVideo ], 'This video contains sensitive content: violence')
await checkCommonVideoListPages('display', [ normalVideo ])
await checkFilterText('warn')
})
it('Should correctly handle display', async () => {
await updateUserNSFW('display', loggedIn)
await checkCommonVideoListPages('display', [ nsfwVideo, violentVideo, normalVideo ])
await checkFilterText('display')
})
it('Should update the setting to blur violent video with display NSFW setting', async () => {
await updateUserViolentNSFW('blur', loggedIn)
await checkCommonVideoListPages('display', [ nsfwVideo, normalVideo ])
await checkCommonVideoListPages('blur', [ violentVideo ])
})
it('Should update the setting to hide NSFW videos but warn violent videos', async () => {
await updateUserNSFW('do_not_list', loggedIn)
await updateUserViolentNSFW('warn', loggedIn)
await checkCommonVideoListPages('display', [ normalVideo ])
await checkCommonVideoListPages('warn', [ violentVideo ])
await checkCommonVideoListPages('do_not_list', [ nsfwVideo ])
})
it('Should update the setting to blur NSFW videos and hide violent videos', async () => {
await updateUserNSFW('blur', loggedIn)
await updateUserViolentNSFW('do_not_list', loggedIn)
await checkCommonVideoListPages('display', [ normalVideo ])
await checkCommonVideoListPages('do_not_list', [ violentVideo ])
await checkCommonVideoListPages('blur', [ nsfwVideo ])
})
}
describe('NSFW with an anonymous user', function () {
runSuite(false)
})
describe('NSFW with a logged in users', function () {
before(async () => {
await loginPage.login({ username: 'user_' + seed, password: 'superpassword' })
})
runSuite(true)
after(async () => {
await loginPage.logout()
})
})
})
})

View File

@@ -0,0 +1,141 @@
import { AdminConfigPage } from '../po/admin-config.po'
import { LoginPage } from '../po/login.po'
import { VideoPublishPage } from '../po/video-publish.po'
import { VideoWatchPage } from '../po/video-watch.po'
import { getScreenshotPath, go, isMobileDevice, isSafari, prepareWebBrowser, selectCustomSelect, waitServerUp } from '../utils'
// These tests help to notice crash with invalid translated strings
describe('Page crash', () => {
let videoPublishPage: VideoPublishPage
let loginPage: LoginPage
let videoWatchPage: VideoWatchPage
let adminConfigPage: AdminConfigPage
const languages = [
'العربية',
'Català',
'Čeština',
'Deutsch',
'ελληνικά',
'Esperanto',
'Español',
'Euskara',
'فارسی',
'Suomi',
'Français',
'Gàidhlig',
'Galego',
'Hrvatski',
'Magyar',
'Íslenska',
'Italiano',
'日本語',
'Taqbaylit',
'Norsk bokmål',
'Nederlands',
'Norsk nynorsk',
'Occitan',
'Polski',
'Português (Brasil)',
'Português (Portugal)',
'Pусский',
'Slovenčina',
'Shqip',
'Svenska',
'ไทย',
'Toki Pona',
'Türkçe',
'украї́нська мо́ва',
'Tiếng Việt',
'简体中文(中国)',
'繁體中文(台灣)'
]
before(async () => {
await waitServerUp()
adminConfigPage = new AdminConfigPage()
loginPage = new LoginPage(isMobileDevice())
videoPublishPage = new VideoPublishPage()
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
await prepareWebBrowser()
})
for (const language of languages) {
describe('For language: ' + language, () => {
describe('Logged in user', () => {
before(async () => {
await loginPage.loginAsRootUser()
})
it('Should change the language', async function () {
await go('/')
await $('.settings-button').waitForClickable()
await $('.settings-button').click()
await selectCustomSelect('language', language)
await $('my-user-interface-settings .primary-button').waitForClickable()
await $('my-user-interface-settings .primary-button').click()
})
it('Should upload and watch a video', async function () {
await videoPublishPage.navigateTo()
await videoPublishPage.uploadVideo('video3.mp4')
await videoPublishPage.validSecondStep('video')
await videoPublishPage.clickOnWatch()
await videoWatchPage.waitWatchVideoName('video')
})
it('Should set a homepage', async function () {
await adminConfigPage.updateHomepage('My custom homepage content')
await adminConfigPage.save()
// All tests
await go('/home')
await $('*=My custom homepage content').waitForDisplayed()
})
it('Should go on overview page and not crash', async function () {
await $('a[href="/videos/overview"]').waitForClickable()
await $('a[href="/videos/overview"]').click()
await $('my-video-overview').waitForExist()
})
it('Should go on videos from subscriptions page', async function () {
await $('a[href="/videos/subscriptions"]').waitForClickable()
await $('a[href="/videos/subscriptions"]').click()
await $('my-videos-user-subscriptions').waitForExist()
})
})
describe('Anonymous user', () => {
before(async () => {
await adminConfigPage.toggleSignup(true)
await adminConfigPage.save()
await loginPage.logout()
await browser.refresh()
})
it('Should go on signup page', async function () {
await $('.create-account-button').waitForClickable()
await $('.create-account-button').click()
await $('.callout-content > h4').waitForExist()
})
})
after(async () => {
await browser.saveScreenshot(getScreenshotPath(`after-page-crash-test-${language}.png`))
})
})
}
})

View File

@@ -0,0 +1,83 @@
import { AnonymousSettingsPage } from '../po/anonymous-settings.po'
import { LoginPage } from '../po/login.po'
import { MyAccountPage } from '../po/my-account.po'
import { VideoPublishPage } from '../po/video-publish.po'
import { VideoWatchPage } from '../po/video-watch.po'
import { go, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
describe('Player settings', () => {
let videoPublishPage: VideoPublishPage
let loginPage: LoginPage
let videoWatchPage: VideoWatchPage
let myAccountPage: MyAccountPage
let anonymousSettingsPage: AnonymousSettingsPage
before(async () => {
await waitServerUp()
loginPage = new LoginPage(isMobileDevice())
videoPublishPage = new VideoPublishPage()
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
myAccountPage = new MyAccountPage()
anonymousSettingsPage = new AnonymousSettingsPage()
await prepareWebBrowser({ hidePrivacyConcerns: false })
})
describe('P2P', function () {
let videoUrl: string
async function goOnVideoWatchPage () {
await go(videoUrl)
await videoWatchPage.waitWatchVideoName('video')
}
async function checkP2P (enabled: boolean) {
await goOnVideoWatchPage()
expect(await videoWatchPage.isPrivacyWarningDisplayed()).toEqual(enabled)
await videoWatchPage.goOnAssociatedEmbed()
expect(await videoWatchPage.isEmbedWarningDisplayed()).toEqual(enabled)
}
before(async () => {
await loginPage.loginAsRootUser()
await videoPublishPage.navigateTo()
await videoPublishPage.uploadVideo('video.mp4')
await videoPublishPage.validSecondStep('video')
await videoPublishPage.clickOnWatch()
await videoWatchPage.waitWatchVideoName('video')
videoUrl = await browser.getUrl()
})
beforeEach(async function () {
await goOnVideoWatchPage()
})
it('Should have P2P enabled for a logged in user', async function () {
await checkP2P(true)
})
it('Should disable P2P for a logged in user', async function () {
await myAccountPage.navigateToMySettings()
await myAccountPage.clickOnP2PCheckbox()
await checkP2P(false)
})
it('Should have P2P enabled for anonymous users', async function () {
await loginPage.logout()
await checkP2P(true)
})
it('Should disable P2P for an anonymous user', async function () {
await anonymousSettingsPage.openSettings()
await anonymousSettingsPage.clickOnP2PCheckbox()
await checkP2P(false)
})
})
})

View File

@@ -0,0 +1,86 @@
import { AdminPluginPage } from '../po/admin-plugin.po'
import { LoginPage } from '../po/login.po'
import { VideoPublishPage } from '../po/video-publish.po'
import { getCheckbox, isMobileDevice, prepareWebBrowser, waitServerUp } from '../utils'
describe('Plugins', () => {
let videoPublishPage: VideoPublishPage
let loginPage: LoginPage
let adminPluginPage: AdminPluginPage
function getPluginCheckbox () {
return getCheckbox('hello-world-field-4')
}
async function expectSubmitError (hasError: boolean) {
await videoPublishPage.clickOnSave()
await $('.form-error*=Should be enabled').waitForDisplayed({ reverse: !hasError })
await $('li*=Should be enabled').waitForDisplayed({ reverse: !hasError })
}
before(async () => {
await waitServerUp()
})
beforeEach(async () => {
loginPage = new LoginPage(isMobileDevice())
videoPublishPage = new VideoPublishPage()
adminPluginPage = new AdminPluginPage()
await prepareWebBrowser()
})
it('Should install hello world plugin', async () => {
await loginPage.loginAsRootUser()
await adminPluginPage.navigateToPluginSearch()
await adminPluginPage.search('hello-world')
await adminPluginPage.installHelloWorld()
await browser.refresh()
})
it('Should have checkbox in video edit page', async () => {
await videoPublishPage.navigateTo()
await videoPublishPage.uploadVideo('video.mp4')
const el = () => $('span=Super field 4 in main tab')
await el().waitForDisplayed()
// Only displayed if the video is public
await videoPublishPage.setAsPrivate()
await el().waitForDisplayed({ reverse: true })
await videoPublishPage.setAsPublic()
await el().waitForDisplayed()
const checkbox = await getPluginCheckbox()
expect(await checkbox.isDisplayed()).toBeTruthy()
await expectSubmitError(true)
})
it('Should check the checkbox and be able to submit the video', async function () {
const checkbox = await getPluginCheckbox()
await checkbox.waitForClickable()
await checkbox.click()
await expectSubmitError(false)
})
it('Should uncheck the checkbox and not be able to submit the video', async function () {
const checkbox = await getPluginCheckbox()
await checkbox.waitForClickable()
await checkbox.click()
await expectSubmitError(true)
})
it('Should change the privacy and should hide the checkbox', async function () {
await videoPublishPage.setAsPrivate()
await expectSubmitError(false)
})
})

View File

@@ -0,0 +1,57 @@
import { AdminConfigPage } from '../po/admin-config.po'
import { LoginPage } from '../po/login.po'
import { VideoPublishPage } from '../po/video-publish.po'
import { VideoWatchPage } from '../po/video-watch.po'
import { isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
describe('Publish live', function () {
let videoPublishPage: VideoPublishPage
let loginPage: LoginPage
let adminConfigPage: AdminConfigPage
let videoWatchPage: VideoWatchPage
before(async () => {
await waitServerUp()
loginPage = new LoginPage(isMobileDevice())
videoPublishPage = new VideoPublishPage()
adminConfigPage = new AdminConfigPage()
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
await prepareWebBrowser()
await loginPage.loginAsRootUser()
})
it('Should enable live', async function () {
await adminConfigPage.toggleLive(true)
await adminConfigPage.save()
})
it('Should create a classic permanent live', async function () {
await videoPublishPage.navigateTo('Go live')
await videoPublishPage.publishLive()
await videoPublishPage.validSecondStep('Permanent live test')
expect(await videoPublishPage.getLiveState()).toEqual('permanent')
await videoPublishPage.clickOnWatch()
await videoWatchPage.waitWatchVideoName('Permanent live test')
})
it('Should create a permanent live and update it to a normal live', async function () {
await videoPublishPage.navigateTo('Go live')
await videoPublishPage.publishLive()
await videoPublishPage.setNormalLive()
await videoPublishPage.validSecondStep('Normal live test')
await videoPublishPage.clickOnWatch()
await videoWatchPage.waitWatchVideoName('Normal live test')
await videoWatchPage.clickOnManage()
expect(await videoPublishPage.getLiveState()).toEqual('normal')
})
})

View File

@@ -0,0 +1,79 @@
import { LoginPage } from '../po/login.po'
import { VideoPublishPage } from '../po/video-publish.po'
import { VideoWatchPage } from '../po/video-watch.po'
import { isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
describe('Publish video', () => {
let videoPublishPage: VideoPublishPage
let loginPage: LoginPage
let videoWatchPage: VideoWatchPage
before(async () => {
await waitServerUp()
loginPage = new LoginPage(isMobileDevice())
videoPublishPage = new VideoPublishPage()
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
await prepareWebBrowser()
await loginPage.loginAsRootUser()
})
describe('Default upload values', function () {
it('Should have default video values', async function () {
await videoPublishPage.navigateTo()
await videoPublishPage.uploadVideo('video3.mp4')
await videoPublishPage.validSecondStep('video')
await videoPublishPage.clickOnWatch()
await videoWatchPage.waitWatchVideoName('video')
expect(await videoWatchPage.getPrivacy()).toBe('Public')
expect(await videoWatchPage.getLicence()).toBe('Unknown')
expect(await videoWatchPage.isDownloadEnabled()).toBeTruthy()
expect(await videoWatchPage.areCommentsEnabled()).toBeTruthy()
})
})
describe('Common', function () {
it('Should upload a video and on refresh being redirected to the manage page', async function () {
await videoPublishPage.navigateTo()
await videoPublishPage.uploadVideo('video.mp4')
await videoPublishPage.validSecondStep('first video')
await videoPublishPage.refresh('first video')
})
it('Should upload a video and schedule upload date', async function () {
await videoPublishPage.navigateTo()
await videoPublishPage.uploadVideo('video.mp4')
await videoPublishPage.scheduleUpload()
await videoPublishPage.validSecondStep('scheduled')
await videoPublishPage.refresh('scheduled')
expect(videoPublishPage.getScheduleInput()).toBeDisplayed()
const nextDay = new Date()
nextDay.setDate(1)
nextDay.setMonth(nextDay.getMonth() + 1)
const inputDate = new Date(await videoPublishPage.getScheduleInput().getValue())
expect(inputDate.getDate()).toEqual(nextDay.getDate())
expect(inputDate.getMonth()).toEqual(nextDay.getMonth())
expect(inputDate.getFullYear()).toEqual(nextDay.getFullYear())
})
})
describe('Import', function () {
it('Should import a video and on refresh being redirected to the manage page', async function () {
await videoPublishPage.navigateTo()
await videoPublishPage.importVideo()
await videoPublishPage.validSecondStep('second video')
await videoPublishPage.refresh('second video')
})
})
})

View File

@@ -0,0 +1,408 @@
import { AdminConfigPage } from '../po/admin-config.po'
import { AdminRegistrationPage } from '../po/admin-registration.po'
import { LoginPage } from '../po/login.po'
import { SignupPage } from '../po/signup.po'
import {
browserSleep,
findEmailTo,
getEmailPort,
getScreenshotPath,
getVerificationLink,
go,
isMobileDevice,
MockSMTPServer,
prepareWebBrowser,
waitServerUp
} from '../utils'
function checkEndMessage (options: {
message: string
requiresEmailVerification: boolean
requiresApproval: boolean
afterEmailVerification: boolean
}) {
const { message, requiresApproval, requiresEmailVerification, afterEmailVerification } = options
{
const created = 'account has been created'
const request = 'account request has been sent'
if (requiresApproval) {
expect(message).toContain(request)
expect(message).not.toContain(created)
} else {
expect(message).not.toContain(request)
expect(message).toContain(created)
}
}
{
const checkEmail = 'Check your email'
if (requiresEmailVerification) {
expect(message).toContain(checkEmail)
} else {
expect(message).not.toContain(checkEmail)
const moderatorsApproval = 'moderator will check your registration request'
if (requiresApproval) {
expect(message).toContain(moderatorsApproval)
} else {
expect(message).not.toContain(moderatorsApproval)
}
}
}
{
const emailVerified = 'email has been verified'
if (afterEmailVerification) {
expect(message).toContain(emailVerified)
} else {
expect(message).not.toContain(emailVerified)
}
}
}
describe('Signup', () => {
let loginPage: LoginPage
let adminConfigPage: AdminConfigPage
let signupPage: SignupPage
let adminRegistrationPage: AdminRegistrationPage
async function prepareSignup (options: {
enabled: boolean
requiresApproval?: boolean
requiresEmailVerification?: boolean
}) {
await loginPage.loginAsRootUser()
// Ensure we change the state of the form to "dirty" so we can save the form
await adminConfigPage.toggleSignup(options.enabled)
if (options.enabled) {
if (options.requiresApproval !== undefined) {
await adminConfigPage.toggleSignupApproval(options.requiresApproval)
}
if (options.requiresEmailVerification !== undefined) {
await adminConfigPage.toggleSignupEmailVerification(options.requiresEmailVerification)
}
}
await adminConfigPage.save()
await loginPage.logout()
await browser.refresh()
}
before(async () => {
await waitServerUp()
})
beforeEach(async () => {
loginPage = new LoginPage(isMobileDevice())
adminConfigPage = new AdminConfigPage()
signupPage = new SignupPage()
adminRegistrationPage = new AdminRegistrationPage()
await prepareWebBrowser()
})
describe('Signup disabled', function () {
it('Should disable signup', async () => {
await prepareSignup({ enabled: false })
await expect(signupPage.getRegisterMenuButton()).not.toBeDisplayed()
})
})
describe('Email verification disabled', function () {
describe('Direct registration', function () {
it('Should enable signup without approval', async () => {
await prepareSignup({ enabled: true, requiresApproval: false, requiresEmailVerification: false })
await signupPage.getRegisterMenuButton().waitForDisplayed()
})
it('Should go on signup page', async function () {
await signupPage.clickOnRegisterButton()
})
it('Should validate the first step (about page)', async function () {
await signupPage.validateStep()
})
it('Should validate the second step (terms)', async function () {
await signupPage.checkTerms()
await signupPage.validateStep()
})
it('Should validate the third step (account)', async function () {
await signupPage.fillAccountStep({ username: 'user_1', displayName: 'user_1_dn' })
await signupPage.validateStep()
})
it('Should validate the third step (channel)', async function () {
await signupPage.fillChannelStep({ name: 'user_1_channel' })
await signupPage.validateStep()
})
it('Should be logged in', async function () {
await loginPage.ensureIsLoggedInAs('user_1_dn')
})
it('Should have a valid end message', async function () {
const message = await signupPage.getEndMessage()
checkEndMessage({
message,
requiresEmailVerification: false,
requiresApproval: false,
afterEmailVerification: false
})
await browser.saveScreenshot(getScreenshotPath('direct-without-email.png'))
await loginPage.logout()
})
})
describe('Registration with approval', function () {
it('Should enable signup with approval', async () => {
await prepareSignup({ enabled: true, requiresApproval: true, requiresEmailVerification: false })
await signupPage.getRegisterMenuButton().waitForDisplayed()
})
it('Should go on signup page', async function () {
await signupPage.clickOnRegisterButton()
})
it('Should validate the first step (about page)', async function () {
await signupPage.validateStep()
})
it('Should validate the second step (terms)', async function () {
await signupPage.checkTerms()
await signupPage.fillRegistrationReason('my super reason')
await signupPage.validateStep()
})
it('Should validate the third step (account)', async function () {
await signupPage.fillAccountStep({ username: 'user_2', displayName: 'user_2 display name', password: 'superpassword' })
await signupPage.validateStep()
})
it('Should validate the third step (channel)', async function () {
await signupPage.fillChannelStep({ name: 'user_2_channel' })
await signupPage.validateStep()
})
it('Should have a valid end message', async function () {
const message = await signupPage.getEndMessage()
checkEndMessage({
message,
requiresEmailVerification: false,
requiresApproval: true,
afterEmailVerification: false
})
await browser.saveScreenshot(getScreenshotPath('request-without-email.png'))
})
it('Should display a message when trying to login with this account', async function () {
const error = await loginPage.getLoginError('user_2', 'superpassword')
expect(error).toContain('awaiting approval')
})
it('Should accept the registration', async function () {
await loginPage.loginAsRootUser()
await adminRegistrationPage.navigateToRegistrationsList()
await adminRegistrationPage.accept('user_2', 'moderation response')
await loginPage.logout()
})
it('Should be able to login with this new account', async function () {
await loginPage.login({ username: 'user_2', password: 'superpassword', displayName: 'user_2 display name' })
await loginPage.logout()
})
})
})
describe('Email verification enabled', function () {
const emails: any[] = []
before(async () => {
await MockSMTPServer.Instance.collectEmails(await getEmailPort(), emails)
})
describe('Direct registration', function () {
it('Should enable signup without approval', async () => {
await prepareSignup({ enabled: true, requiresApproval: false, requiresEmailVerification: true })
await signupPage.getRegisterMenuButton().waitForDisplayed()
})
it('Should go on signup page', async function () {
await signupPage.clickOnRegisterButton()
})
it('Should validate the first step (about page)', async function () {
await signupPage.validateStep()
})
it('Should validate the second step (terms)', async function () {
await signupPage.checkTerms()
await signupPage.validateStep()
})
it('Should validate the third step (account)', async function () {
await signupPage.fillAccountStep({ username: 'user_3', displayName: 'user_3 display name', email: 'user_3@example.com' })
await signupPage.validateStep()
})
it('Should validate the third step (channel)', async function () {
await signupPage.fillChannelStep({ name: 'user_3_channel' })
await signupPage.validateStep()
})
it('Should have a valid end message', async function () {
const message = await signupPage.getEndMessage()
checkEndMessage({
message,
requiresEmailVerification: true,
requiresApproval: false,
afterEmailVerification: false
})
await browser.saveScreenshot(getScreenshotPath('direct-with-email.png'))
})
it('Should validate the email', async function () {
let email: { text: string }
while (!(email = findEmailTo(emails, 'user_3@example.com'))) {
await browserSleep(100)
}
await go(getVerificationLink(email))
const message = await signupPage.getEndMessage()
checkEndMessage({
message,
requiresEmailVerification: false,
requiresApproval: false,
afterEmailVerification: true
})
await browser.saveScreenshot(getScreenshotPath('direct-after-email.png'))
})
})
describe('Registration with approval', function () {
it('Should enable signup without approval', async () => {
await prepareSignup({ enabled: true, requiresApproval: true, requiresEmailVerification: true })
await signupPage.getRegisterMenuButton().waitForDisplayed()
})
it('Should go on signup page', async function () {
await signupPage.clickOnRegisterButton()
})
it('Should validate the first step (about page)', async function () {
await signupPage.validateStep()
})
it('Should validate the second step (terms)', async function () {
await signupPage.checkTerms()
await signupPage.fillRegistrationReason('my super reason 2')
await signupPage.validateStep()
})
it('Should validate the third step (account)', async function () {
await signupPage.fillAccountStep({
username: 'user_4',
displayName: 'user_4 display name',
email: 'user_4@example.com',
password: 'superpassword'
})
await signupPage.validateStep()
})
it('Should validate the third step (channel)', async function () {
await signupPage.fillChannelStep({ name: 'user_4_channel' })
await signupPage.validateStep()
})
it('Should have a valid end message', async function () {
const message = await signupPage.getEndMessage()
checkEndMessage({
message,
requiresEmailVerification: true,
requiresApproval: true,
afterEmailVerification: false
})
await browser.saveScreenshot(getScreenshotPath('request-with-email.png'))
})
it('Should display a message when trying to login with this account', async function () {
const error = await loginPage.getLoginError('user_4', 'superpassword')
expect(error).toContain('awaiting approval')
})
it('Should accept the registration', async function () {
await loginPage.loginAsRootUser()
await adminRegistrationPage.navigateToRegistrationsList()
await adminRegistrationPage.accept('user_4', 'moderation response 2')
await loginPage.logout()
})
it('Should validate the email', async function () {
let email: { text: string }
while (!(email = findEmailTo(emails, 'user_4@example.com'))) {
await browserSleep(100)
}
await go(getVerificationLink(email))
const message = await signupPage.getEndMessage()
checkEndMessage({
message,
requiresEmailVerification: false,
requiresApproval: true,
afterEmailVerification: true
})
await browser.saveScreenshot(getScreenshotPath('request-after-email.png'))
})
})
after(() => {
MockSMTPServer.Instance.kill()
})
})
after(async () => {
await browser.saveScreenshot(getScreenshotPath('after-test.png'))
})
})

View File

@@ -0,0 +1,76 @@
import { AdminConfigPage } from '../po/admin-config.po'
import { LoginPage } from '../po/login.po'
import { MyAccountPage } from '../po/my-account.po'
import {
browserSleep,
findEmailTo,
getEmailPort,
getVerificationLink,
go,
isMobileDevice,
MockSMTPServer,
prepareWebBrowser,
waitServerUp
} from '../utils'
describe('User settings', () => {
let loginPage: LoginPage
let myAccountPage: MyAccountPage
let adminConfigPage: AdminConfigPage
const emails: any[] = []
before(async () => {
await waitServerUp()
loginPage = new LoginPage(isMobileDevice())
myAccountPage = new MyAccountPage()
adminConfigPage = new AdminConfigPage()
await MockSMTPServer.Instance.collectEmails(await getEmailPort(), emails)
await prepareWebBrowser()
})
describe('Email', function () {
before(async function () {
await loginPage.loginAsRootUser()
await adminConfigPage.toggleSignup(true)
await adminConfigPage.toggleSignupEmailVerification(true)
await adminConfigPage.save()
await browser.refresh()
})
it('Should ask to change the email', async function () {
await myAccountPage.navigateToMySettings()
await myAccountPage.updateEmail('email2@example.com', loginPage.getRootPassword())
const pendingEmailBlock = $('.pending-email')
await pendingEmailBlock.waitForDisplayed()
await expect(pendingEmailBlock).toHaveText(expect.stringContaining('email2@example.com is awaiting email verification'))
let email: { text: string }
while (!(email = findEmailTo(emails, 'email2@example.com'))) {
await browserSleep(100)
}
await go(getVerificationLink(email))
const alertBlock = $('.alert-success')
await alertBlock.waitForDisplayed()
await expect(alertBlock).toHaveText(expect.stringContaining('Email updated'))
await myAccountPage.navigateToMySettings()
const changeEmailBlock = $('.change-email .form-group-description')
await changeEmailBlock.waitForDisplayed()
await expect(changeEmailBlock).toHaveText(expect.stringContaining('Your current email is email2@example.com'))
})
})
after(() => {
MockSMTPServer.Instance.kill()
})
})

View File

@@ -0,0 +1,226 @@
import { LoginPage } from '../po/login.po'
import { MyAccountPage } from '../po/my-account.po'
import { PlayerPage } from '../po/player.po'
import { SignupPage } from '../po/signup.po'
import { VideoPublishPage } from '../po/video-publish.po'
import { VideoWatchPage } from '../po/video-watch.po'
import { go, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
describe('Password protected videos', () => {
let videoPublishPage: VideoPublishPage
let loginPage: LoginPage
let videoWatchPage: VideoWatchPage
let signupPage: SignupPage
let playerPage: PlayerPage
let myAccountPage: MyAccountPage
let passwordProtectedVideoUrl: string
let playlistUrl: string
const seed = Math.random()
const passwordProtectedVideoName = seed + ' - password protected'
const publicVideoName1 = seed + ' - public 1'
const publicVideoName2 = seed + ' - public 2'
const videoPassword = 'password'
const regularUsername = 'user_1'
const regularUserPassword = 'user password'
const playlistName = seed + ' - playlist'
function testRateAndComment () {
it('Should add and remove like on video', async function () {
await videoWatchPage.like()
await videoWatchPage.like()
})
it('Should create thread on video', async function () {
await videoWatchPage.createThread('My first comment')
})
it('Should reply to thread on video', async function () {
await videoWatchPage.createReply('My first reply')
})
}
before(async () => {
await waitServerUp()
loginPage = new LoginPage(isMobileDevice())
videoPublishPage = new VideoPublishPage()
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
signupPage = new SignupPage()
playerPage = new PlayerPage()
myAccountPage = new MyAccountPage()
await prepareWebBrowser()
})
describe('Owner', function () {
before(async () => {
await loginPage.loginAsRootUser()
})
it('Should login, upload a public video and save it to a playlist', async () => {
await videoPublishPage.navigateTo()
await videoPublishPage.uploadVideo('video.mp4')
await videoPublishPage.validSecondStep(publicVideoName1)
await videoPublishPage.clickOnWatch()
await videoWatchPage.waitWatchVideoName(publicVideoName1)
await videoWatchPage.clickOnSave()
await videoWatchPage.createPlaylist(playlistName)
await videoWatchPage.saveToPlaylist(playlistName)
await browser.pause(5000)
})
it('Should upload a password protected video', async () => {
await videoPublishPage.navigateTo()
await videoPublishPage.uploadVideo('video2.mp4')
await videoPublishPage.setAsPasswordProtected(videoPassword)
await videoPublishPage.validSecondStep(passwordProtectedVideoName)
await videoPublishPage.clickOnWatch()
await videoWatchPage.waitWatchVideoName(passwordProtectedVideoName)
passwordProtectedVideoUrl = await browser.getUrl()
})
it('Should save to playlist the password protected video', async () => {
await videoWatchPage.clickOnSave()
await videoWatchPage.saveToPlaylist(playlistName)
})
it('Should upload a second public video and save it to playlist', async () => {
await videoPublishPage.navigateTo()
await videoPublishPage.uploadVideo('video3.mp4')
await videoPublishPage.validSecondStep(publicVideoName2)
await videoPublishPage.clickOnWatch()
await videoWatchPage.waitWatchVideoName(publicVideoName2)
await videoWatchPage.clickOnSave()
await videoWatchPage.saveToPlaylist(playlistName)
})
it('Should play video without password', async function () {
await go(passwordProtectedVideoUrl)
await videoWatchPage.waitWatchVideoName(passwordProtectedVideoName)
expect(await videoWatchPage.getPrivacy()).toBe('Password protected')
await playerPage.playAndPauseVideo(false, 2)
})
testRateAndComment()
it('Should play video on embed without password', async function () {
await videoWatchPage.goOnAssociatedEmbed()
await playerPage.playAndPauseVideo(false, 2)
})
it('Should have the playlist in my account', async function () {
await go('/')
await myAccountPage.navigateToMyPlaylists()
const videosNumberText = await myAccountPage.getPlaylistVideosText(playlistName)
expect(videosNumberText).toEqual('3 videos')
await myAccountPage.clickOnPlaylist(playlistName)
const count = await myAccountPage.countTotalPlaylistElements()
expect(count).toEqual(3)
})
it('Should update the playlist to public', async () => {
const url = await browser.getUrl()
const regex = /\/my-library\/video-playlists\/([^/]+)/i
const match = url.match(regex)
const uuid = match ? match[1] : null
expect(uuid).not.toBeNull()
await myAccountPage.updatePlaylistPrivacy(uuid, 'Public')
})
it('Should watch the playlist', async () => {
await myAccountPage.clickOnPlaylist(playlistName)
await myAccountPage.playPlaylist()
await videoWatchPage.waitWatchVideoName(publicVideoName1, 40 * 1000)
playlistUrl = await browser.getUrl()
await videoWatchPage.waitWatchVideoName(passwordProtectedVideoName, 40 * 1000)
await videoWatchPage.waitWatchVideoName(publicVideoName2, 40 * 1000)
})
after(async () => {
await loginPage.logout()
})
})
describe('Regular users', function () {
before(async () => {
await signupPage.fullSignup({
accountInfo: {
username: regularUsername,
password: regularUserPassword
},
channelInfo: {
name: 'user_1_channel'
}
})
})
it('Should requires password to play video', async function () {
await go(passwordProtectedVideoUrl)
await videoWatchPage.fillVideoPassword(videoPassword)
await videoWatchPage.waitWatchVideoName(passwordProtectedVideoName)
expect(await videoWatchPage.getPrivacy()).toBe('Password protected')
await playerPage.playAndPauseVideo(true, 2)
})
testRateAndComment()
it('Should requires password to play video on embed', async function () {
await videoWatchPage.goOnAssociatedEmbed(true)
await playerPage.fillEmbedVideoPassword(videoPassword)
await playerPage.playAndPauseVideo(false, 2)
})
it('Should watch the playlist without password protected video', async () => {
await go(playlistUrl)
await playerPage.playVideo()
await videoWatchPage.waitWatchVideoName(publicVideoName2, 40 * 1000)
})
after(async () => {
await loginPage.logout()
})
})
describe('Anonymous users', function () {
it('Should requires password to play video', async function () {
await go(passwordProtectedVideoUrl)
await videoWatchPage.fillVideoPassword(videoPassword)
await videoWatchPage.waitWatchVideoName(passwordProtectedVideoName)
expect(await videoWatchPage.getPrivacy()).toBe('Password protected')
await playerPage.playAndPauseVideo(true, 2)
})
it('Should requires password to play video on embed', async function () {
await videoWatchPage.goOnAssociatedEmbed(true)
await playerPage.fillEmbedVideoPassword(videoPassword)
await playerPage.playAndPauseVideo(false, 2)
})
it('Should watch the playlist without password protected video', async () => {
await go(playlistUrl)
await playerPage.playVideo()
await videoWatchPage.waitWatchVideoName(publicVideoName2, 40 * 1000)
})
})
})

9
client/e2e/src/types/wdio.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
declare global {
namespace WebdriverIO {
interface Element {
chooseFile: (path: string) => Promise<void>
}
}
}
export {}

View File

@@ -0,0 +1,67 @@
export async function browserSleep (amount: number) {
await browser.pause(amount)
}
// ---------------------------------------------------------------------------
export function isMobileDevice () {
const platformName = (browser.capabilities['platformName'] || '').toLowerCase()
return platformName === 'android' || platformName === 'ios'
}
export function isAndroid () {
const platformName = (browser.capabilities['platformName'] || '').toLowerCase()
return platformName === 'android'
}
export function isSafari () {
return browser.capabilities['browserName'] &&
browser.capabilities['browserName'].toLowerCase() === 'safari'
}
export function isIOS () {
return isMobileDevice() && isSafari()
}
export async function go (url: string) {
await browser.url(url)
await browser.execute(() => {
const style = document.createElement('style')
style.innerHTML = 'p-toast { display: none }'
document.head.appendChild(style)
})
}
// ---------------------------------------------------------------------------
export async function prepareWebBrowser (options: {
hidePrivacyConcerns?: boolean // default true
} = {}) {
const { hidePrivacyConcerns = true } = options
if (hidePrivacyConcerns) {
try {
await browser.execute(() => {
localStorage.setItem('video-watch-privacy-concern', 'true')
})
} catch {
console.log('Cannot set local storage to hide privacy concerns')
}
}
if (!isMobileDevice() && process.env.MOZ_HEADLESS_WIDTH) {
await browser.setWindowSize(+process.env.MOZ_HEADLESS_WIDTH, +process.env.MOZ_HEADLESS_HEIGHT)
}
}
export async function waitServerUp () {
await browser.waitUntil(async () => {
await go('/')
await browserSleep(500)
return $('<my-app>').isDisplayed()
}, { timeout: 20 * 1000 })
}

View File

@@ -0,0 +1,76 @@
export async function getCheckbox (name: string) {
const input = $(`my-peertube-checkbox input[id=${name}]`)
await input.waitForExist()
return input.parentElement()
}
export function isCheckboxSelected (name: string) {
return $(`input[id=${name}]`).isSelected()
}
export async function setCheckboxEnabled (name: string, enabled: boolean) {
if (await isCheckboxSelected(name) === enabled) return
const checkbox = await getCheckbox(name)
await checkbox.scrollIntoView({ block: 'center' })
await checkbox.waitForClickable()
await checkbox.click()
}
// ---------------------------------------------------------------------------
export async function isRadioSelected (name: string) {
await $(`input[id=${name}] + label`).waitForClickable()
return $(`input[id=${name}]`).isSelected()
}
export async function clickOnRadio (name: string) {
const label = $(`input[id=${name}] + label`)
await label.waitForClickable()
await label.click()
}
// ---------------------------------------------------------------------------
export async function selectCustomSelect (id: string, valueLabel: string) {
const wrapper = $(`[formcontrolname=${id}] span[role=combobox]`)
await wrapper.waitForExist()
await wrapper.scrollIntoView({ block: 'center' })
await wrapper.waitForClickable()
await wrapper.click()
const getOption = async () => {
const options = await $$(`[formcontrolname=${id}] li[role=option]`).filter(async o => {
const text = await o.getText()
return text.trimStart().startsWith(valueLabel)
})
if (options.length === 0) return undefined
return options[0]
}
await browser.waitUntil(async () => {
const option = await getOption()
if (!option) return false
return option.isDisplayed()
})
return (await getOption()).click()
}
export async function findParentElement (
el: ChainablePromiseElement,
finder: (el: ChainablePromiseElement) => Promise<boolean>
) {
if (await finder(el) === true) return el
return findParentElement(el.parentElement(), finder)
}

View File

@@ -0,0 +1,38 @@
export function getVerificationLink (email: { text: string }) {
const { text } = email
const regexp = /\[(?<link>http:\/\/[^\]]+)\]/g
const matched = text.matchAll(regexp)
if (!matched) throw new Error('Could not find verification link in email')
for (const match of matched) {
const link = match.groups.link
if (link.includes('/verify-account/')) {
return link.replace('127.0.0.1', 'localhost')
}
}
throw new Error('Could not find /verify-account/ link')
}
export function findEmailTo (emails: { text: string, to: { address: string }[] }[], to: string) {
for (const email of emails) {
for (const { address } of email.to) {
if (address === to) return email
}
}
return undefined
}
export async function getEmailPort () {
const key = browser.options.baseUrl + '-emailPort'
// FIXME: typings are wrong, get returns a promise
// FIXME: use * because the key is not properly escaped by the shared store when using get(key)
const emailPort = (await (browser.sharedStore.get('*') as unknown as Promise<number>))[key]
if (!emailPort) throw new Error('Invalid email port')
return emailPort
}

View File

@@ -0,0 +1,13 @@
import { mkdir, rm } from 'node:fs/promises'
import { join } from 'node:path'
const SCREENSHOTS_DIRECTORY = 'screenshots'
export async function createScreenshotsDirectory () {
await rm(SCREENSHOTS_DIRECTORY, { recursive: true, force: true })
await mkdir(SCREENSHOTS_DIRECTORY, { recursive: true })
}
export function getScreenshotPath (filename: string) {
return join(SCREENSHOTS_DIRECTORY, filename)
}

View File

@@ -0,0 +1,113 @@
import { setValue } from '@wdio/shared-store-service'
import { ChildProcessWithoutNullStreams } from 'node:child_process'
import { basename } from 'node:path'
import { createScreenshotsDirectory, getScreenshotPath } from './files'
import { runCommand, runServer } from './server'
let appInstance: number
let app: ChildProcessWithoutNullStreams
let emailPort: number
async function beforeLocalSuite (suite: any) {
const config = buildConfig(suite.file)
await runCommand('npm run clean:server:test -- ' + appInstance)
app = runServer(appInstance, config)
}
function afterLocalSuite () {
app.kill()
app = undefined
}
async function afterLocalTest (test: { file: string }) {
const filename = basename(test.file).replace(/\.ts$/, '')
await browser.saveScreenshot(getScreenshotPath(`${filename}-after-test.png`))
}
async function beforeLocalSession (config: { baseUrl: string }, capabilities: { browserName: string }) {
createScreenshotsDirectory()
appInstance = capabilities['browserName'] === 'chrome'
? 1
: 2
emailPort = 1025 + appInstance
config.baseUrl = 'http://localhost:900' + appInstance
await setValue(config.baseUrl + '-emailPort', emailPort)
}
async function onBrowserStackPrepare () {
const appInstance = 1
await runCommand('npm run clean:server:test -- ' + appInstance)
app = runServer(appInstance)
}
function onBrowserStackComplete () {
app.kill()
app = undefined
}
export {
afterLocalSuite,
afterLocalTest,
beforeLocalSession,
beforeLocalSuite,
onBrowserStackComplete,
onBrowserStackPrepare
}
// ---------------------------------------------------------------------------
function buildConfig (suiteFile: string = undefined) {
const filename = basename(suiteFile)
if (filename === 'custom-server-defaults.e2e-spec.ts') {
return {
defaults: {
publish: {
download_enabled: false,
comments_policy: 2,
privacy: 2,
licence: 4
},
p2p: {
webapp: {
enabled: false
},
embed: {
enabled: false
}
}
}
}
}
if (filename === 'signup.e2e-spec.ts' || filename === 'user-settings.e2e-spec.ts') {
return {
signup: {
limit: -1
},
smtp: {
hostname: '127.0.0.1',
port: emailPort
}
}
}
if (filename === 'video-password.e2e-spec.ts') {
return {
signup: {
enabled: true,
limit: -1
}
}
}
return {}
}

View File

@@ -0,0 +1,8 @@
export * from './common'
export * from './elements'
export * from './email'
export * from './files'
export * from './hooks'
export * from './mock-smtp'
export * from './server'
export * from './urls'

View File

@@ -0,0 +1,57 @@
import MailDev from '@peertube/maildev'
class MockSMTPServer {
private static instance: MockSMTPServer
private started = false
private maildev: any
private emails: object[]
collectEmails (port: number, emailsCollection: object[]) {
return new Promise<number>((res, rej) => {
this.emails = emailsCollection
if (this.started) {
return res(undefined)
}
this.maildev = new MailDev({
ip: '127.0.0.1',
smtp: port,
disableWeb: true,
silent: true
})
this.maildev.on('new', email => {
this.emails.push(email)
})
this.maildev.listen(err => {
if (err) return rej(err)
this.started = true
return res(port)
})
})
}
kill () {
if (!this.maildev) return
this.maildev.close()
this.maildev = null
MockSMTPServer.instance = null
}
static get Instance () {
return this.instance || (this.instance = new this())
}
}
// ---------------------------------------------------------------------------
export {
MockSMTPServer
}

View File

@@ -0,0 +1,68 @@
import { exec, spawn } from 'node:child_process'
import { join, resolve } from 'node:path'
function runServer (appInstance: number, config: any = {}) {
const env = Object.create(process.env)
env['NODE_OPTIONS'] = ''
env['NODE_ENV'] = 'test'
env['NODE_APP_INSTANCE'] = appInstance + ''
env['NODE_CONFIG'] = JSON.stringify({
rates_limit: {
api: {
max: 5000
},
login: {
max: 5000
}
},
log: {
level: 'warn'
},
transcoding: {
enabled: false
},
video_studio: {
enabled: false
},
...config
})
const forkOptions = {
env,
cwd: getRootCWD(),
detached: false
}
const p = spawn('node', [ join('dist', 'server.js') ], forkOptions)
p.stderr.on('data', data => console.error(data.toString()))
p.stdout.on('data', data => console.error(data.toString()))
return p
}
function runCommand (command: string) {
return new Promise<void>((res, rej) => {
// Reset NODE_OPTIONS env set by webdriverio
const env = { ...process.env, NODE_OPTIONS: '' }
const p = exec(command, { env, cwd: getRootCWD() })
p.stderr.on('data', data => console.error(data.toString()))
p.on('error', err => rej(err))
p.on('exit', () => res())
})
}
export {
runServer,
runCommand
}
// ---------------------------------------------------------------------------
function getRootCWD () {
return resolve('../..')
}

View File

@@ -0,0 +1,23 @@
const FIXTURE_URLS = {
INTERNAL_WEB_VIDEO: 'https://peertube2.cpy.re/w/pwfz7NizSdPD4mJcbbmNwa?mode=web-video&start=0',
INTERNAL_HLS_VIDEO: 'https://peertube2.cpy.re/w/pwfz7NizSdPD4mJcbbmNwa?start=0',
INTERNAL_EMBED_WEB_VIDEO: 'https://peertube2.cpy.re/videos/embed/pwfz7NizSdPD4mJcbbmNwa?mode=web-video&start=0',
INTERNAL_EMBED_HLS_VIDEO: 'https://peertube2.cpy.re/videos/embed/pwfz7NizSdPD4mJcbbmNwa?start=0',
INTERNAL_HLS_ONLY_VIDEO: 'https://peertube2.cpy.re/w/tKQmHcqdYZRdCszLUiWM3V?start=0',
INTERNAL_EMBED_HLS_ONLY_VIDEO: 'https://peertube2.cpy.re/videos/embed/tKQmHcqdYZRdCszLUiWM3V?start=0',
WEB_VIDEO: 'https://peertube2.cpy.re/w/122d093a-1ede-43bd-bd34-59d2931ffc5e',
HLS_EMBED: 'https://peertube2.cpy.re/videos/embed/969bf103-7818-43b5-94a0-de159e13de50',
HLS_PLAYLIST_EMBED: 'https://peertube2.cpy.re/video-playlists/embed/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a',
LIVE_VIDEO: 'https://peertube2.cpy.re/w/oBw6LwsMWWRkmXYfuYRpJd',
IMPORT_URL: 'https://download.cpy.re/peertube/good_video.mp4'
}
export {
FIXTURE_URLS
}

28
client/e2e/tsconfig.json Normal file
View File

@@ -0,0 +1,28 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"noImplicitAny": false,
"esModuleInterop": true,
"module": "commonjs",
"target": "ES2018",
"typeRoots": [
"../node_modules/@wdio",
"../node_modules/@types",
"../node_modules"
],
"types": [
"node",
"@wdio/globals/types",
"@wdio/mocha-framework",
"expect-webdriverio"
]
},
"ts-node": {
"files": true
},
"include": [
"src/**/*.ts",
"./*.ts"
]
}

View File

@@ -0,0 +1,179 @@
import { onBrowserStackComplete, onBrowserStackPrepare } from './src/utils'
import { config as mainConfig } from './wdio.main.conf'
const user = process.env.BROWSERSTACK_USER
const key = process.env.BROWSERSTACK_KEY
if (!user) throw new Error('Miss browser stack user')
if (!key) throw new Error('Miss browser stack key')
function buildMainOptions (sessionName: string) {
return {
projectName: 'PeerTube',
buildName: 'Main E2E - ' + new Date().toISOString(),
sessionName,
consoleLogs: 'info',
networkLogs: true
}
}
function buildBStackDesktopOptions (options: {
sessionName: string
resolution: string
os?: string
osVersion?: string
}) {
const { sessionName, resolution, os, osVersion } = options
return {
'bstack:options': {
...buildMainOptions(sessionName),
os,
osVersion,
resolution
}
}
}
function buildBStackMobileOptions (options: {
sessionName: string
deviceName: string
osVersion: string
}) {
const { sessionName, deviceName, osVersion } = options
return {
'bstack:options': {
...buildMainOptions(sessionName),
realMobile: true,
osVersion,
deviceName
}
}
}
module.exports = {
config: {
...mainConfig,
user,
key,
maxInstances: 5,
capabilities: [
{
browserName: 'Chrome',
...buildBStackDesktopOptions({ sessionName: 'Latest Chrome Desktop', resolution: '1280x1024', os: 'Windows', osVersion: '8' })
},
{
browserName: 'Firefox',
browserVersion: '79', // Oldest supported version
...buildBStackDesktopOptions({ sessionName: 'Firefox ESR Desktop', resolution: '1280x1024', os: 'Windows', osVersion: '8' })
},
{
browserName: 'Safari',
browserVersion: '14',
...buildBStackDesktopOptions({ sessionName: 'Safari Desktop', resolution: '1280x1024' })
},
{
browserName: 'Firefox',
...buildBStackDesktopOptions({ sessionName: 'Firefox Latest', resolution: '1280x1024', os: 'Windows', osVersion: '8' })
},
{
browserName: 'Edge',
...buildBStackDesktopOptions({ sessionName: 'Edge Latest', resolution: '1280x1024' })
},
{
browserName: 'Chrome',
...buildBStackMobileOptions({ sessionName: 'Latest Chrome Android', deviceName: 'Samsung Galaxy S10', osVersion: '9.0' })
},
{
browserName: 'Safari',
...buildBStackMobileOptions({ sessionName: 'Safari iPhone', deviceName: 'iPhone 12', osVersion: '14' })
},
{
browserName: 'Safari',
...buildBStackMobileOptions({ sessionName: 'Safari iPad', deviceName: 'iPad Pro 12.9 2021', osVersion: '14' })
}
],
host: 'hub-cloud.browserstack.com',
connectionRetryTimeout: 240000,
waitforTimeout: 20000,
specs: [
// We don't want to test "local" tests
'./src/suites-all/*.e2e-spec.ts'
],
services: [
[
'browserstack',
{ browserstackLocal: true }
]
],
onWorkerStart: function (_cid, capabilities) {
if (capabilities['bstack:options'].realMobile === true) {
capabilities['bstack:options'].local = false
}
},
before: function () {
require('./src/commands/upload')
// Force keep alive: https://www.browserstack.com/docs/automate/selenium/error-codes/keep-alive-not-used#Node_JS
const http = require('node:http')
const https = require('node:https')
const keepAliveTimeout = 30 * 1000
// eslint-disable-next-line no-prototype-builtins
if (http.globalAgent?.hasOwnProperty('keepAlive')) {
http.globalAgent.keepAlive = true
https.globalAgent.keepAlive = true
http.globalAgent.keepAliveMsecs = keepAliveTimeout
https.globalAgent.keepAliveMsecs = keepAliveTimeout
} else {
const agent = new http.Agent({
keepAlive: true,
keepAliveMsecs: keepAliveTimeout
})
const secureAgent = new https.Agent({
keepAlive: true,
keepAliveMsecs: keepAliveTimeout
})
const httpRequest = http.request
const httpsRequest = https.request
http.request = function (options, callback) {
if (options.protocol === 'https:') {
options['agent'] = secureAgent
return httpsRequest(options, callback)
} else {
options['agent'] = agent
return httpRequest(options, callback)
}
}
}
},
onPrepare: onBrowserStackPrepare,
onComplete: onBrowserStackComplete
} as WebdriverIO.Config
}

View File

@@ -0,0 +1,53 @@
import { afterLocalSuite, afterLocalTest, beforeLocalSession, beforeLocalSuite } from './src/utils'
import { config as mainConfig } from './wdio.main.conf'
const prefs = {
'intl.accept_languages': 'en'
}
// Chrome headless does not support prefs
process.env.LANG = 'en'
// https://github.com/mozilla/geckodriver/issues/1354#issuecomment-479456411
process.env.MOZ_HEADLESS_WIDTH = '1280'
process.env.MOZ_HEADLESS_HEIGHT = '1024'
const windowSizeArg = `--window-size=${process.env.MOZ_HEADLESS_WIDTH},${process.env.MOZ_HEADLESS_HEIGHT}`
module.exports = {
config: {
...mainConfig,
runner: 'local',
maxInstances: 1,
specFileRetries: 0,
capabilities: [
{
'browserName': 'chrome',
'acceptInsecureCerts': true,
'goog:chromeOptions': {
args: [ '--disable-gpu', windowSizeArg ],
prefs
}
}
// {
// 'browserName': 'firefox',
// 'moz:firefoxOptions': {
// binary: '/usr/bin/firefox-developer-edition',
// args: [ '--headless', windowSizeArg ],
// prefs
// }
// }
],
services: [ 'shared-store' ],
beforeSession: beforeLocalSession,
beforeSuite: beforeLocalSuite,
afterSuite: afterLocalSuite,
afterTest: afterLocalTest
} as WebdriverIO.Config
}

View File

@@ -0,0 +1,48 @@
import { afterLocalSuite, afterLocalTest, beforeLocalSession, beforeLocalSuite } from './src/utils'
import { config as mainConfig } from './wdio.main.conf'
const prefs = { 'intl.accept_languages': 'en' }
process.env.LANG = 'en'
// https://github.com/mozilla/geckodriver/issues/1354#issuecomment-479456411
process.env.MOZ_HEADLESS_WIDTH = '1280'
process.env.MOZ_HEADLESS_HEIGHT = '1024'
const windowSizeArg = `--window-size=${process.env.MOZ_HEADLESS_WIDTH},${process.env.MOZ_HEADLESS_HEIGHT}`
module.exports = {
config: {
...mainConfig,
runner: 'local',
maxInstancesPerCapability: 1,
capabilities: [
{
'browserName': 'chrome',
'goog:chromeOptions': {
binary: '/usr/bin/google-chrome-stable',
args: [ '--headless', '--disable-gpu', windowSizeArg ],
prefs
}
},
{
'browserName': 'firefox',
'moz:firefoxOptions': {
binary: '/usr/bin/firefox-developer-edition',
args: [ '--headless', windowSizeArg ],
prefs
}
}
],
services: [ 'shared-store' ],
beforeSession: beforeLocalSession,
beforeSuite: beforeLocalSuite,
afterSuite: afterLocalSuite,
afterTest: afterLocalTest
} as WebdriverIO.Config
}

View File

@@ -0,0 +1,110 @@
export const config = {
//
// ====================
// Runner Configuration
// ====================
//
//
// ==================
// Specify Test Files
// ==================
// Define which test specs should run. The pattern is relative to the directory
// from which `wdio` was called.
//
// The specs are defined as an array of spec files (optionally using wildcards
// that will be expanded). The test for each spec file will be run in a separate
// worker process. In order to have a group of spec files run in the same worker
// process simply enclose them in an array within the specs array.
//
// If you are calling `wdio` from an NPM script (see https://docs.npmjs.com/cli/run-script),
// then the current working directory is where your `package.json` resides, so `wdio`
// will be called from there.
//
specs: [
'./src/suites-all/*.e2e-spec.ts',
'./src/suites-local/*.e2e-spec.ts'
],
// Patterns to exclude.
exclude: [
// 'path/to/excluded/files'
],
//
// ===================
// Test Configurations
// ===================
// Define all options that are relevant for the WebdriverIO instance here
//
// Level of logging verbosity: trace | debug | info | warn | error | silent
logLevel: 'info',
//
// Set specific log levels per logger
// loggers:
// - webdriver, webdriverio
// - @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service
// - @wdio/mocha-framework, @wdio/jasmine-framework
// - @wdio/local-runner
// - @wdio/sumologic-reporter
// - @wdio/cli, @wdio/config, @wdio/utils
// Level of logging verbosity: trace | debug | info | warn | error | silent
// logLevels: {
// webdriver: 'info',
// '@wdio/appium-service': 'info'
// },
//
// If you only want to run your tests until a specific amount of tests have failed use
// bail (default is 0 - don't bail, run all tests).
bail: 0,
//
// Set a base URL in order to shorten url command calls. If your `url` parameter starts
// with `/`, the base url gets prepended, not including the path portion of your baseUrl.
// If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
// gets prepended directly.
baseUrl: 'http://127.0.0.1:9001',
//
// Default timeout for all waitFor* commands.
waitforTimeout: 5000,
//
// Default timeout in milliseconds for request
// if browser driver or grid doesn't send response
connectionRetryTimeout: 120000,
//
// Default request retries count
connectionRetryCount: 3,
// Framework you want to run your specs with.
// The following are supported: Mocha, Jasmine, and Cucumber
// see also: https://webdriver.io/docs/frameworks
//
// Make sure you have the wdio adapter package for the specific framework installed
// before running any tests.
framework: 'mocha',
//
// The number of times to retry the entire specfile when it fails as a whole
specFileRetries: 2,
//
// Delay in seconds between the spec file retry attempts
// specFileRetriesDelay: 0,
//
// Whether or not retried specfiles should be retried immediately or deferred to the end of the queue
// specFileRetriesDeferred: false,
//
// Test reporter for stdout.
// The only one supported by default is 'dot'
// see also: https://webdriver.io/docs/dot-reporter
reporters: [ 'spec' ],
//
// Options to be passed to Mocha.
// See the full list at http://mochajs.org/
mochaOpts: {
ui: 'bdd',
timeout: 60000,
bail: true
},
tsConfigPath: require('node:path').join(__dirname, './tsconfig.json'),
before: function () {
require('./src/commands/upload')
}
} as Partial<WebdriverIO.Config>