Migrate to webdriverio

pull/4387/head
Chocobozzz 2021-08-30 16:24:25 +02:00
parent 2a4c9669d2
commit 3419e0e1fe
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
25 changed files with 2399 additions and 1173 deletions

View File

@ -22,6 +22,8 @@
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"jsdoc/newline-after-description": "off",
"jsdoc/check-alignment": "off",
"lines-between-class-members": "off",
"@typescript-eslint/lines-between-class-members": [ "off" ],
"arrow-body-style": "off",

1
client/.gitignore vendored
View File

@ -8,3 +8,4 @@
/src/locale/target/iso639_*.xml
/src/locale/target/player_*.xml
/src/locale/target/server_*.xml
/e2e/local.log

View File

@ -233,21 +233,6 @@
"with": "src/environments/environment.hmr.ts"
}
]
},
"e2e": {
"localize": false,
"budgets": [
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.e2e.ts"
}
]
}
}
},
@ -266,10 +251,6 @@
},
"ar-locale": {
"browserTarget": "PeerTube:build:ar-locale"
},
"e2e": {
"browserTarget": "PeerTube:build:e2e",
"proxyConfig": "e2e/proxy.config.json"
}
}
},
@ -361,25 +342,6 @@
}
}
}
},
"PeerTube-e2e": {
"root": "e2e/",
"sourceRoot": "",
"projectType": "application",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "PeerTube:serve:e2e"
},
"configurations": {
"local": {
"protractorConfig": "e2e/local-protractor.conf.js"
}
}
}
}
}
},
"defaultProject": "PeerTube",

View File

@ -1,56 +0,0 @@
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const {SpecReporter} = require('jasmine-spec-reporter')
exports.config = {
allScriptsTimeout: 25000,
specs: ['./src/**/*.e2e-spec.ts'],
directConnect: true,
multiCapabilities: [
{
'browserName': 'firefox',
'name': 'Firefox',
'moz:firefoxOptions': {
binary: '/usr/bin/firefox-developer-edition',
// args: ["-headless"],
log: {
"level": "info" // default is "info"
}
}
},
{
'browserName': 'firefox',
'name': 'Firefox ESR',
'moz:firefoxOptions': {
binary: '/usr/bin/firefox-esr',
// args: ["-headless"],
log: {
"level": "info" // default is "info"
}
}
},
{
'browserName': 'chrome',
'name': 'Chromium'
}
],
maxSessions: 1,
baseUrl: 'http://localhost:3000/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 45000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
})
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }))
}
}

View File

@ -1,96 +0,0 @@
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const {SpecReporter} = require('jasmine-spec-reporter')
exports.config = {
allScriptsTimeout: 25000,
specs: [ './src/**/*.e2e-spec.ts' ],
seleniumAddress: 'http://hub-cloud.browserstack.com/wd/hub',
commonCapabilities: {
'browserstack.user': process.env.BROWSERSTACK_USER,
'browserstack.key': process.env.BROWSERSTACK_KEY,
'browserstack.local': true,
'browserstack.console': 'verbose',
'browserstack.networkLogs': true,
'browserstack.debug': true,
project: 'PeerTube',
build: 'Main',
name: 'Bstack-[Protractor] Parallel Test'
},
multiCapabilities: [
{
browserName: 'Safari',
version: '11.1',
name: 'Safari Desktop',
resolution: '1280x1024'
},
{
browserName: 'Chrome',
name: 'Latest Chrome Desktop',
resolution: '1280x1024'
},
{
browserName: 'Firefox',
version: '68', // ESR
name: 'Firefox ESR Desktop',
resolution: '1280x1024'
},
{
browserName: 'Firefox',
name: 'Latest Firefox Desktop',
resolution: '1280x1024'
},
{
browserName: 'Edge',
name: 'Latest Edge Desktop',
resolution: '1280x1024'
},
{
browserName: 'Chrome',
device: 'Google Nexus 6',
real_mobile: 'true',
os_version: '5.0',
name: 'Latest Chrome Android'
},
{
browserName: 'Safari',
device: 'iPhone 8 Plus',
real_mobile: 'true',
os_version: '11',
name: 'Safari iPhone'
},
{
browserName: 'Safari',
device: 'iPad 7th',
real_mobile: 'true',
os_version: '13',
name: 'Safari iPad'
}
],
// maxSessions: 1,
// BrowserStack compatible ports: https://www.browserstack.com/question/664
baseUrl: 'http://localhost:3333/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 45000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
})
jasmine.getEnv().addReporter(new SpecReporter({
spec: { displayStacktrace: 'raw' }
}))
}
}
exports.config.multiCapabilities.forEach(function (caps) {
for (var i in exports.config.commonCapabilities) caps[i] = caps[i] || exports.config.commonCapabilities[i]
})

View File

@ -1,37 +0,0 @@
{
"/api": {
"target": "http://localhost:9000",
"secure": false
},
"/plugins": {
"target": "http://localhost:9000",
"secure": false
},
"/themes": {
"target": "http://localhost:9000",
"secure": false
},
"/static": {
"target": "http://localhost:9000",
"secure": false
},
"/lazy-static": {
"target": "http://localhost:9000",
"secure": false
},
"/socket.io": {
"target": "ws://localhost:9000",
"secure": false,
"ws": true
},
"/!(client)**": {
"target": "http://localhost:3333/client/index.html",
"secure": false,
"logLevel": "debug"
},
"/!(client)**/**": {
"target": "http://localhost:3333/client/index.html",
"secure": false,
"logLevel": "debug"
}
}

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

@ -1,23 +1,25 @@
import { browser, element, by } from 'protractor'
import { go } from '../utils'
export class LoginPage {
async loginAsRootUser () {
await browser.get('/login')
await go('/login')
await browser.executeScript(`window.localStorage.setItem('no_instance_config_warning_modal', 'true')`)
await browser.executeScript(`window.localStorage.setItem('no_welcome_modal', 'true')`)
await browser.execute(`window.localStorage.setItem('no_instance_config_warning_modal', 'true')`)
await browser.execute(`window.localStorage.setItem('no_welcome_modal', 'true')`)
element(by.css('input#username')).sendKeys('root')
element(by.css('input#password')).sendKeys('test1')
await $('input#username').setValue('root')
await $('input#password').setValue('test1')
await browser.sleep(1000)
await browser.pause(1000)
await element(by.css('form input[type=submit]')).click()
await $('form input[type=submit]').click()
expect(this.getLoggedInInfo().getText()).toContain('root')
await this.getLoggedInInfoElem().waitForExist()
await expect(this.getLoggedInInfoElem()).toHaveText('root')
}
private getLoggedInInfo () {
return element(by.css('.logged-in-display-name'))
private getLoggedInInfoElem () {
return $('.logged-in-display-name')
}
}

View File

@ -1,85 +1,117 @@
import { by, element, browser } from 'protractor'
import { go } from '../utils'
export class MyAccountPage {
navigateToMyVideos () {
return element(by.css('a[href="/my-library/videos"]')).click()
return $('a[href="/my-library/videos"]').click()
}
navigateToMyPlaylists () {
return element(by.css('a[href="/my-library/video-playlists"]')).click()
return $('a[href="/my-library/video-playlists"]').click()
}
navigateToMyHistory () {
return element(by.css('a[href="/my-library/history/videos"]')).click()
return $('a[href="/my-library/history/videos"]').click()
}
// My account Videos
async removeVideo (name: string) {
const container = this.getVideoElement(name)
const container = await this.getVideoElement(name)
await container.element(by.css('.dropdown-toggle')).click()
await container.$('.dropdown-toggle').click()
const dropdownMenu = container.element(by.css('.dropdown-menu .dropdown-item:nth-child(2)'))
await browser.wait(browser.ExpectedConditions.presenceOf(dropdownMenu))
const dropdownMenu = () => container.$('.dropdown-menu .dropdown-item:nth-child(2)')
return dropdownMenu.click()
await dropdownMenu().waitForDisplayed()
return dropdownMenu().click()
}
validRemove () {
return element(by.css('input[type=submit]')).click()
return $('input[type=submit]').click()
}
countVideos (names: string[]) {
return element.all(by.css('.video'))
.filter(e => {
return e.element(by.css('.video-miniature-name'))
.getText()
.then(t => names.some(n => t.includes(n)))
})
.count()
async countVideos (names: string[]) {
const elements = await $$('.video').filter(async e => {
const t = await e.$('.video-miniature-name').getText()
return names.some(n => t.includes(n))
})
return elements.length
}
// My account playlists
getPlaylistVideosText (name: string) {
return this.getPlaylist(name).element(by.css('.miniature-playlist-info-overlay')).getText()
async getPlaylistVideosText (name: string) {
const elem = await this.getPlaylist(name)
return elem.$('.miniature-playlist-info-overlay').getText()
}
clickOnPlaylist (name: string) {
return this.getPlaylist(name).element(by.css('.miniature-thumbnail')).click()
async clickOnPlaylist (name: string) {
const elem = await this.getPlaylist(name)
return elem.$('.miniature-thumbnail').click()
}
countTotalPlaylistElements () {
return element.all(by.css('my-video-playlist-element-miniature')).count()
async countTotalPlaylistElements () {
await $('<my-video-playlist-element-miniature>').waitForDisplayed()
return $$('<my-video-playlist-element-miniature>').length
}
playPlaylist () {
return element(by.css('.playlist-info .miniature-thumbnail')).click()
return $('.playlist-info .miniature-thumbnail').click()
}
async goOnAssociatedPlaylistEmbed () {
let url = await browser.getCurrentUrl()
let url = await browser.getUrl()
url = url.replace('/w/p/', '/video-playlists/embed/')
url = url.replace(':3333', ':9001')
return browser.get(url)
return go(url)
}
// My account Videos
private getVideoElement (name: string) {
return element.all(by.css('.video'))
.filter(e => e.element(by.css('.video-miniature-name')).getText().then(t => t.includes(name)))
.first()
private async getVideoElement (name: string) {
const video = async () => {
const videos = await $$('.video').filter(async e => {
const t = await e.$('.video-miniature-name').getText()
return t.includes(name)
})
return videos[0]
}
await browser.waitUntil(async () => {
return (await video()).isDisplayed()
})
return video()
}
// My account playlists
private getPlaylist (name: string) {
return element.all(by.css('my-video-playlist-miniature'))
.filter(e => e.element(by.css('.miniature-name')).getText().then(t => t.includes(name)))
.first()
private async getPlaylist (name: string) {
const playlist = () => {
return $$('my-video-playlist-miniature')
.filter(async e => {
const t = await e.$('.miniature-name').getText()
return t.includes(name)
})
.then(elems => elems[0])
}
await browser.waitUntil(async () => {
const el = await playlist()
return el?.isDisplayed()
})
return playlist()
}
}

View File

@ -1,45 +1,54 @@
import { browser, by, element } from 'protractor'
import { browserSleep, isIOS, isMobileDevice, isSafari } from '../utils'
export class PlayerPage {
async getWatchVideoPlayerCurrentTime () {
const elem = element(by.css('video'))
getWatchVideoPlayerCurrentTime () {
const elem = $('video')
return elem.getAttribute('currentTime')
if (isIOS()) {
return elem.getAttribute('currentTime')
.then(t => parseInt(t, 10))
.then(t => Math.round(t))
}
return elem.getProperty('currentTime')
}
waitUntilPlaylistInfo (text: string) {
const elem = element(by.css('.video-js .vjs-playlist-info'))
return browser.wait(browser.ExpectedConditions.textToBePresentInElement(elem, text))
waitUntilPlaylistInfo (text: string, maxTime: number) {
return browser.waitUntil(async () => {
return (await $('.video-js .vjs-playlist-info').getText()).includes(text)
}, { timeout: maxTime })
}
waitUntilPlayerWrapper () {
const elem = element(by.css('#placeholder-preview'))
return browser.wait(browser.ExpectedConditions.presenceOf(elem))
return browser.waitUntil(async () => {
return !!(await $('#placeholder-preview'))
})
}
async playAndPauseVideo (isAutoplay: boolean) {
const videojsEl = element(by.css('div.video-js'))
await browser.wait(browser.ExpectedConditions.elementToBeClickable(videojsEl))
const videojsElem = () => $('div.video-js')
await videojsElem().waitForExist()
// Autoplay is disabled on iOS and Safari
if (await isIOS() || await isSafari() || await isMobileDevice()) {
if (isIOS() || isSafari() || isMobileDevice()) {
// We can't play the video using protractor if it is not muted
await browser.executeScript(`document.querySelector('video').muted = true`)
await browser.execute(`document.querySelector('video').muted = true`)
await this.clickOnPlayButton()
} else if (isAutoplay === false) {
await this.clickOnPlayButton()
}
await browserSleep(2000)
await browser.wait(browser.ExpectedConditions.invisibilityOf(element(by.css('.vjs-loading-spinner'))))
await browserSleep(2000)
await browser.waitUntil(async () => {
return !await $('.vjs-loading-spinner').isDisplayedInViewport()
}, { timeout: 20 * 1000 })
await videojsEl.click()
await browserSleep(4000)
await videojsElem().click()
}
async playVideo () {
@ -47,8 +56,9 @@ export class PlayerPage {
}
private async clickOnPlayButton () {
const playButton = element(by.css('.vjs-big-play-button'))
await browser.wait(browser.ExpectedConditions.elementToBeClickable(playButton))
await playButton.click()
const playButton = () => $('.vjs-big-play-button')
await playButton().waitForClickable()
await playButton().click()
}
}

View File

@ -1,11 +1,11 @@
import { by, element } from 'protractor'
export class VideoUpdatePage {
async updateName (videoName: string) {
const nameInput = element(by.css('input#name'))
await nameInput.clear()
await nameInput.sendKeys(videoName)
const nameInput = $('input#name')
await nameInput.waitForDisplayed()
await nameInput.clearValue()
await nameInput.setValue(videoName)
}
async validUpdate () {
@ -15,6 +15,6 @@ export class VideoUpdatePage {
}
private getSubmitButton () {
return element(by.css('.submit-container .action-button'))
return $('.submit-container .action-button')
}
}

View File

@ -1,33 +1,29 @@
import { browser, by, element } from 'protractor'
import { FileDetector } from 'selenium-webdriver/remote'
import { join } from 'path'
export class VideoUploadPage {
async navigateTo () {
await element(by.css('.header .publish-button')).click()
await $('.header .publish-button').click()
return browser.wait(browser.ExpectedConditions.visibilityOf(element(by.css('.upload-video-container'))))
await $('.upload-video-container').waitForDisplayed()
}
async uploadVideo () {
browser.setFileDetector(new FileDetector())
const fileToUpload = join(__dirname, '../../fixtures/video.mp4')
const fileInputSelector = '.upload-video-container input[type=file]'
const parentFileInput = '.upload-video-container .button-file'
// Avoid sending keys on non visible element
await browser.executeScript(`document.querySelector('${fileInputSelector}').style.opacity = 1`)
await browser.executeScript(`document.querySelector('${parentFileInput}').style.overflow = 'initial'`)
await browser.execute(`document.querySelector('${fileInputSelector}').style.opacity = 1`)
await browser.execute(`document.querySelector('${parentFileInput}').style.overflow = 'initial'`)
await browser.sleep(1000)
await browser.pause(1000)
const elem = element(by.css(fileInputSelector))
await elem.sendKeys(fileToUpload)
const elem = await $(fileInputSelector)
await elem.chooseFile(fileToUpload)
// Wait for the upload to finish
await browser.wait(async () => {
const actionButton = this.getSecondStepSubmitButton().element(by.css('.action-button'))
await browser.waitUntil(async () => {
const actionButton = this.getSecondStepSubmitButton().$('.action-button')
const klass = await actionButton.getAttribute('class')
return !klass.includes('disabled')
@ -35,16 +31,18 @@ export class VideoUploadPage {
}
async validSecondUploadStep (videoName: string) {
const nameInput = element(by.css('input#name'))
await nameInput.clear()
await nameInput.sendKeys(videoName)
const nameInput = $('input#name')
await nameInput.clearValue()
await nameInput.setValue(videoName)
await this.getSecondStepSubmitButton().click()
return browser.wait(browser.ExpectedConditions.urlContains('/w/'))
return browser.waitUntil(async () => {
return (await browser.getUrl()).includes('/w/')
})
}
private getSecondStepSubmitButton () {
return element(by.css('.submit-container my-button'))
return $('.submit-container my-button')
}
}

View File

@ -1,5 +1,4 @@
import { browser, by, element, ElementFinder, ExpectedConditions } from 'protractor'
import { browserSleep, isMobileDevice } from '../utils'
import { browserSleep, go } from '../utils'
export class VideoWatchPage {
async goOnVideosList (isMobileDevice: boolean, isSafari: boolean) {
@ -12,19 +11,19 @@ export class VideoWatchPage {
url = '/videos/recently-added'
}
await browser.get(url, 20000)
await go(url)
// Waiting the following element does not work on Safari...
if (isSafari) return browserSleep(3000)
const elem = element.all(by.css('.videos .video-miniature .video-miniature-name')).first()
return browser.wait(browser.ExpectedConditions.visibilityOf(elem))
await $('.videos .video-miniature .video-miniature-name').waitForDisplayed()
}
getVideosListName () {
return element.all(by.css('.videos .video-miniature .video-miniature-name'))
.getText()
.then((texts: any) => texts.map((t: any) => t.trim()))
async getVideosListName () {
const elems = await $$('.videos .video-miniature .video-miniature-name')
const texts = await Promise.all(elems.map(e => e.getText()))
return texts.map(t => t.trim())
}
waitWatchVideoName (videoName: string, isMobileDevice: boolean, isSafari: boolean) {
@ -33,99 +32,134 @@ export class VideoWatchPage {
// On mobile we display the first node, on desktop the second
const index = isMobileDevice ? 0 : 1
const elem = element.all(by.css('.video-info .video-info-name')).get(index)
return browser.wait(browser.ExpectedConditions.textToBePresentInElement(elem, videoName))
return browser.waitUntil(async () => {
return (await $$('.video-info .video-info-name')[index].getText()).includes(videoName)
})
}
getVideoName () {
return this.getVideoNameElement().getText()
return this.getVideoNameElement().then(e => e.getText())
}
async goOnAssociatedEmbed () {
let url = await browser.getCurrentUrl()
let url = await browser.getUrl()
url = url.replace('/w/', '/videos/embed/')
url = url.replace(':3333', ':9001')
return browser.get(url)
return go(url)
}
async goOnP2PMediaLoaderEmbed () {
return browser.get('https://peertube2.cpy.re/videos/embed/969bf103-7818-43b5-94a0-de159e13de50')
goOnP2PMediaLoaderEmbed () {
return go(
'https://peertube2.cpy.re/videos/embed/969bf103-7818-43b5-94a0-de159e13de50'
)
}
async goOnP2PMediaLoaderPlaylistEmbed () {
return browser.get('https://peertube2.cpy.re/video-playlists/embed/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a')
goOnP2PMediaLoaderPlaylistEmbed () {
return go(
'https://peertube2.cpy.re/video-playlists/embed/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a'
)
}
async clickOnVideo (videoName: string) {
const video = element.all(by.css('.videos .video-miniature .video-miniature-name'))
.filter(e => e.getText().then(t => t === videoName ))
.first()
const video = async () => {
const videos = await $$('.videos .video-miniature .video-miniature-name').filter(async e => {
const t = await e.getText()
await browser.wait(browser.ExpectedConditions.elementToBeClickable(video))
await video.click()
return t === videoName
})
await browser.wait(browser.ExpectedConditions.urlContains('/w/'))
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 = element.all(by.css('.videos .video-miniature .video-thumbnail')).first()
const videoName = element.all(by.css('.videos .video-miniature .video-miniature-name')).first()
const video = () => $('.videos .video-miniature .video-thumbnail')
const videoName = () => $('.videos .video-miniature .video-miniature-name')
// Don't know why but the expectation fails on Safari
await browser.wait(browser.ExpectedConditions.elementToBeClickable(video))
await video().waitForClickable()
const textToReturn = videoName.getText()
await video.click()
const textToReturn = await videoName().getText()
await video().click()
await browser.waitUntil(async () => (await browser.getUrl()).includes('/w/'))
await browser.wait(browser.ExpectedConditions.urlContains('/w/'))
return textToReturn
}
async clickOnUpdate () {
const dropdown = element(by.css('my-video-actions-dropdown .action-button'))
const dropdown = $('my-video-actions-dropdown .action-button')
await dropdown.click()
const items: ElementFinder[] = await element.all(by.css('.dropdown-menu.show .dropdown-item'))
await $('.dropdown-menu.show .dropdown-item').waitForDisplayed()
const items = await $$('.dropdown-menu.show .dropdown-item')
for (const item of items) {
const href = await item.getAttribute('href')
if (href && href.includes('/update/')) {
if (href?.includes('/update/')) {
await item.click()
return
}
}
}
async clickOnSave () {
return element(by.css('.action-button-save')).click()
clickOnSave () {
return $('.action-button-save').click()
}
async createPlaylist (name: string) {
await element(by.css('.new-playlist-button')).click()
const newPlaylistButton = () => $('.new-playlist-button')
await element(by.css('#displayName')).sendKeys(name)
await newPlaylistButton().waitForClickable()
await newPlaylistButton().click()
return element(by.css('.new-playlist-block input[type=submit]')).click()
const displayName = () => $('#displayName')
await displayName().waitForDisplayed()
await displayName().setValue(name)
return $('.new-playlist-block input[type=submit]').click()
}
async saveToPlaylist (name: string) {
return element.all(by.css('my-video-add-to-playlist .playlist'))
.filter(p => p.getText().then(t => t === name))
.click()
const playlist = () => $('my-video-add-to-playlist').$(`.playlist=${name}`)
await playlist().waitForDisplayed()
return playlist().click()
}
waitUntilVideoName (name: string, maxTime: number) {
const elem = this.getVideoNameElement()
return browser.wait(ExpectedConditions.textToBePresentInElement(elem, name), maxTime)
return browser.waitUntil(async () => {
return (await this.getVideoName()) === name
}, { timeout: maxTime })
}
private getVideoNameElement () {
private async getVideoNameElement () {
// We have 2 video info name block, pick the first that is not empty
return element.all(by.css('.video-info-first-row .video-info-name'))
.filter(e => e.getText().then(t => !!t))
.first()
const elem = async () => {
const elems = await $$('.video-info-first-row .video-info-name').filter(e => e.isDisplayed())
return elems[0]
}
await browser.waitUntil(async () => {
const e = await elem()
return e?.isDisplayed()
})
return elem()
}
}

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

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

View File

@ -1,34 +1,37 @@
import { browser } from 'protractor'
async function browserSleep (amount: number) {
const oldValue = await browser.waitForAngularEnabled()
// iOS does not seem to work with protractor
// https://github.com/angular/protractor/issues/2840
if (await isIOS()) browser.waitForAngularEnabled(true)
await browser.sleep(amount)
if (await isIOS()) browser.waitForAngularEnabled(oldValue)
await browser.pause(amount)
}
async function isMobileDevice () {
const caps = await browser.getCapabilities()
return caps.get('realMobile') === 'true' || caps.get('realMobile') === true
function isMobileDevice () {
const platformName = (browser.capabilities['platformName'] || '').toLowerCase()
return platformName === 'android' || platformName === 'ios'
}
async function isSafari () {
const caps = await browser.getCapabilities()
return caps.get('browserName') && caps.get('browserName').toLowerCase() === 'safari'
function isSafari () {
return browser.capabilities['browserName'] &&
browser.capabilities['browserName'].toLowerCase() === 'safari'
}
async function isIOS () {
return await isMobileDevice() && await isSafari()
function isIOS () {
return isMobileDevice() && isSafari()
}
export {
async function go (url: string) {
await browser.url(url)
// Hide notifications that could fail tests when hiding buttons
await browser.execute(() => {
const style = document.createElement('style')
style.innerHTML = 'p-toast { display: none }'
document.head.appendChild(style)
})
}
export {
isMobileDevice,
isSafari,
isIOS,
go,
browserSleep
}

View File

@ -1,14 +1,13 @@
import { browser } from 'protractor'
import { LoginPage } from './po/login.po'
import { MyAccountPage } from './po/my-account'
import { PlayerPage } from './po/player.po'
import { VideoUpdatePage } from './po/video-update.po'
import { VideoUploadPage } from './po/video-upload.po'
import { VideoWatchPage } from './po/video-watch.po'
import { isIOS, isMobileDevice, isSafari } from './utils'
import { browserSleep, go, isIOS, isMobileDevice, isSafari } from './utils'
async function skipIfUploadNotSupported () {
if (await isMobileDevice() || await isSafari()) {
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
}
@ -24,11 +23,30 @@ describe('Videos workflow', () => {
let loginPage: LoginPage
let playerPage: PlayerPage
let videoName = new Date().getTime() + ' video'
const video2Name = new Date().getTime() + ' second video'
const playlistName = new Date().getTime() + ' playlist'
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 browser.waitUntil(async () => {
await go('/')
await browserSleep(500)
return $('<my-app>').isDisplayed()
}, { timeout: 20 * 1000 })
})
beforeEach(async () => {
videoWatchPage = new VideoWatchPage()
videoUploadPage = new VideoUploadPage()
@ -37,25 +55,13 @@ describe('Videos workflow', () => {
loginPage = new LoginPage()
playerPage = new PlayerPage()
if (await isIOS()) {
// iOS does not seem to work with protractor
// https://github.com/angular/protractor/issues/2840
browser.waitForAngularEnabled(false)
console.log('iOS detected')
} else if (await isMobileDevice()) {
console.log('Android detected.')
} else if (await isSafari()) {
console.log('Safari detected.')
}
if (!await isMobileDevice()) {
await browser.driver.manage().window().maximize()
if (!isMobileDevice()) {
await browser.maximizeWindow()
}
})
it('Should log in', async () => {
if (await isMobileDevice() || await isSafari()) {
if (isMobileDevice() || isSafari()) {
console.log('Skipping because we are on a real device or Safari and BrowserStack does not support file upload.')
return
}
@ -64,7 +70,7 @@ describe('Videos workflow', () => {
})
it('Should upload a video', async () => {
if (await skipIfUploadNotSupported()) return
if (isUploadUnsupported()) return
await videoUploadPage.navigateTo()
@ -73,62 +79,52 @@ describe('Videos workflow', () => {
})
it('Should list videos', async () => {
await videoWatchPage.goOnVideosList(await isMobileDevice(), await isSafari())
await videoWatchPage.goOnVideosList(isMobileDevice(), isSafari())
if (await skipIfUploadNotSupported()) return
if (isUploadUnsupported()) return
const videoNames = videoWatchPage.getVideosListName()
const videoNames = await videoWatchPage.getVideosListName()
expect(videoNames).toContain(videoName)
})
it('Should go on video watch page', async () => {
let videoNameToExcept = videoName
if (await isMobileDevice() || await isSafari()) {
await browser.get('https://peertube2.cpy.re/w/122d093a-1ede-43bd-bd34-59d2931ffc5e')
if (isMobileDevice() || isSafari()) {
await go('https://peertube2.cpy.re/w/122d093a-1ede-43bd-bd34-59d2931ffc5e')
videoNameToExcept = 'E2E tests'
} else {
await videoWatchPage.clickOnVideo(videoName)
}
return videoWatchPage.waitWatchVideoName(videoNameToExcept, await isMobileDevice(), await isSafari())
return videoWatchPage.waitWatchVideoName(videoNameToExcept, isMobileDevice(), isSafari())
})
it('Should play the video', async () => {
videoWatchUrl = await browser.getCurrentUrl()
videoWatchUrl = await browser.getUrl()
await playerPage.playAndPauseVideo(true)
expect(playerPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2)
expect(await playerPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2)
})
it('Should watch the associated embed video', async () => {
const oldValue = await browser.waitForAngularEnabled()
await browser.waitForAngularEnabled(false)
await videoWatchPage.goOnAssociatedEmbed()
await playerPage.playAndPauseVideo(false)
expect(playerPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2)
await browser.waitForAngularEnabled(oldValue)
expect(await playerPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2)
})
it('Should watch the p2p media loader embed video', async () => {
const oldValue = await browser.waitForAngularEnabled()
await browser.waitForAngularEnabled(false)
await videoWatchPage.goOnP2PMediaLoaderEmbed()
await playerPage.playAndPauseVideo(false)
expect(playerPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2)
await browser.waitForAngularEnabled(oldValue)
expect(await playerPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2)
})
it('Should update the video', async () => {
if (await skipIfUploadNotSupported()) return
if (isUploadUnsupported()) return
await browser.get(videoWatchUrl)
await go(videoWatchUrl)
await videoWatchPage.clickOnUpdate()
@ -142,14 +138,14 @@ describe('Videos workflow', () => {
})
it('Should add the video in my playlist', async () => {
if (await skipIfUploadNotSupported()) return
if (isUploadUnsupported()) return
await videoWatchPage.clickOnSave()
await videoWatchPage.createPlaylist(playlistName)
await videoWatchPage.saveToPlaylist(playlistName)
await browser.sleep(5000)
await browser.pause(5000)
await videoUploadPage.navigateTo()
@ -161,7 +157,7 @@ describe('Videos workflow', () => {
})
it('Should have the playlist in my account', async () => {
if (await skipIfUploadNotSupported()) return
if (isUploadUnsupported()) return
await myAccountPage.navigateToMyPlaylists()
@ -175,26 +171,18 @@ describe('Videos workflow', () => {
})
it('Should watch the playlist', async () => {
if (await skipIfUploadNotSupported()) return
if (isUploadUnsupported()) return
await myAccountPage.playPlaylist()
const oldValue = await browser.waitForAngularEnabled()
await browser.waitForAngularEnabled(false)
await videoWatchPage.waitUntilVideoName(video2Name, 20000 * 1000)
await browser.waitForAngularEnabled(oldValue)
await videoWatchPage.waitUntilVideoName(video2Name, 30 * 1000)
})
it('Should watch the webtorrent playlist in the embed', async () => {
if (await skipIfUploadNotSupported()) return
if (isUploadUnsupported()) return
const accessToken = await browser.executeScript(`return window.localStorage.getItem('access_token');`)
const refreshToken = await browser.executeScript(`return window.localStorage.getItem('refresh_token');`)
const oldValue = await browser.waitForAngularEnabled()
await browser.waitForAngularEnabled(false)
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()
@ -202,49 +190,45 @@ describe('Videos workflow', () => {
console.log('Will set %s and %s tokens in local storage.', accessToken, refreshToken)
await browser.executeScript(`window.localStorage.setItem('access_token', '${accessToken}');`)
await browser.executeScript(`window.localStorage.setItem('refresh_token', '${refreshToken}');`)
await browser.executeScript(`window.localStorage.setItem('token_type', 'Bearer');`)
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')
await browser.waitForAngularEnabled(oldValue)
await playerPage.waitUntilPlaylistInfo('2/2', 30 * 1000)
})
it('Should watch the HLS playlist in the embed', async () => {
const oldValue = await browser.waitForAngularEnabled()
await browser.waitForAngularEnabled(false)
await videoWatchPage.goOnP2PMediaLoaderPlaylistEmbed()
await playerPage.playVideo()
await playerPage.waitUntilPlaylistInfo('2/2')
await browser.waitForAngularEnabled(oldValue)
await playerPage.waitUntilPlaylistInfo('2/2', 30 * 1000)
})
it('Should delete the video 2', async () => {
if (await skipIfUploadNotSupported()) return
if (isUploadUnsupported()) return
// Go to the dev website
await browser.get(videoWatchUrl)
await go(videoWatchUrl)
await myAccountPage.navigateToMyVideos()
await myAccountPage.removeVideo(video2Name)
await myAccountPage.validRemove()
const count = await myAccountPage.countVideos([ videoName, video2Name ])
expect(count).toEqual(1)
await browser.waitUntil(async () => {
const count = await myAccountPage.countVideos([ videoName, video2Name ])
return count === 1
})
})
it('Should delete the first video', async () => {
if (await skipIfUploadNotSupported()) return
if (isUploadUnsupported()) return
await myAccountPage.removeVideo(videoName)
await myAccountPage.validRemove()

View File

@ -5,9 +5,14 @@
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
"node",
"webdriverio/async",
"@wdio/mocha-framework",
"expect-webdriverio"
]
}
},
"include": [
"src/**/*.ts",
"./*.ts"
]
}

View File

@ -0,0 +1,114 @@
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().split('T')[0],
sessionName,
consoleLogs: 'info',
networkLogs: true
}
}
function buildBStackDesktopOptions (sessionName: string, resolution?: string) {
return {
'bstack:options': {
...buildMainOptions(sessionName),
resolution
}
}
}
function buildBStackMobileOptions (sessionName: string, deviceName: string, osVersion: string) {
return {
'bstack:options': {
...buildMainOptions(sessionName),
realMobile: true,
osVersion,
deviceName
}
}
}
module.exports = {
config: {
...mainConfig,
user,
key,
maxInstances: 5,
capabilities: [
{
browserName: 'Chrome',
...buildBStackDesktopOptions('Latest Chrome Desktop', '1280x1024')
},
{
browserName: 'Firefox',
browserVersion: '68', // ESR
...buildBStackDesktopOptions('Firefox ESR Desktop', '1280x1024')
},
{
browserName: 'Safari',
browserVersion: '11.1',
...buildBStackDesktopOptions('Safari Desktop', '1280x1024')
},
{
browserName: 'Firefox',
...buildBStackDesktopOptions('Firefox Latest', '1280x1024')
},
{
browserName: 'Edge',
...buildBStackDesktopOptions('Edge Latest', '1280x1024')
},
{
browserName: 'Chrome',
...buildBStackMobileOptions('Latest Chrome Android', 'Samsung Galaxy S6', '5.0')
},
{
browserName: 'Safari',
...buildBStackMobileOptions('Safari iPhone', 'iPhone 8 Plus', '11')
},
{
browserName: 'Safari',
...buildBStackMobileOptions('Safari iPad', 'iPad 7th', '13')
}
],
host: 'hub-cloud.browserstack.com',
connectionRetryTimeout: 240000,
waitforTimeout: 20000,
services: [
[
'browserstack', { browserstackLocal: true }
]
],
after: function (result) {
if (result === 0) {
browser.executeScript('browserstack_executor: {"action": "setSessionStatus", "arguments": {"status":"passed","reason": ""}}', [])
} else {
browser.executeScript('browserstack_executor: {"action": "setSessionStatus", "arguments": {"status":"failed","reason": ""}}', [])
}
}
} as WebdriverIO.Config
}

View File

@ -0,0 +1,43 @@
import { config as mainConfig } from './wdio.main.conf'
const prefs = {
'intl.accept_languages': 'en'
}
module.exports = {
config: {
...mainConfig,
runner: 'local',
maxInstances: 1,
capabilities: [
{
browserName: 'chrome',
acceptInsecureCerts: true,
'goog:chromeOptions': {
prefs
}
},
{
browserName: 'firefox',
'moz:firefoxOptions': {
// args: [ '-headless' ],
binary: '/usr/bin/firefox-developer-edition',
prefs
}
},
{
browserName: 'firefox',
'moz:firefoxOptions': {
// args: [ '-headless' ],
binary: '/usr/bin/firefox-esr',
prefs
}
}
],
services: [ 'chromedriver', 'geckodriver' ]
} as WebdriverIO.Config
}

View File

@ -0,0 +1,115 @@
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/**/*.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: 1,
//
// 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://localhost: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: 1,
//
// 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
},
autoCompileOpts: {
autoCompile: true,
tsNodeOpts: {
project: require('path').join(__dirname, './tsconfig.json')
}
},
before: function () {
require('expect-webdriverio')
require('./src/commands/upload')
}
} as Partial<WebdriverIO.Config>

View File

@ -58,8 +58,6 @@
"@types/chart.js": "^2.9.16",
"@types/core-js": "^2.5.2",
"@types/debug": "^4.1.5",
"@types/jasmine": "^3.3.15",
"@types/jasminewd2": "^2.0.3",
"@types/jschannel": "^1.0.0",
"@types/linkifyjs": "^2.1.2",
"@types/lodash-es": "^4.17.0",
@ -71,12 +69,19 @@
"@types/webtorrent": "^0.109.0",
"@typescript-eslint/eslint-plugin": "4.29.3",
"@typescript-eslint/parser": "4.29.3",
"@wdio/browserstack-service": "^7.11.1",
"@wdio/cli": "^7.11.1",
"@wdio/codemod": "^0.9.0",
"@wdio/local-runner": "^7.11.1",
"@wdio/mocha-framework": "^7.11.1",
"@wdio/spec-reporter": "^7.10.1",
"angular2-hotkeys": "^2.1.2",
"angularx-qrcode": "11.0.0",
"bootstrap": "^4.1.3",
"buffer": "^6.0.3",
"cache-chunk-store": "^3.0.0",
"chart.js": "^3.5.1",
"chromedriver": "^92.0.1",
"core-js": "^3.1.4",
"css-loader": "^6.2.0",
"debug": "^4.3.1",
@ -85,19 +90,15 @@
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jsdoc": "^36.0.8",
"eslint-plugin-prefer-arrow": "latest",
"expect": "^27.1.0",
"expect-webdriverio": "^3.1.2",
"focus-visible": "^5.0.2",
"geckodriver": "^2.0.3",
"hls.js": "^1.0.7",
"html-loader": "^2.1.2",
"html-webpack-plugin": "^5.3.1",
"https-browserify": "^1.0.0",
"jasmine-core": "~3.8.0",
"jasmine-spec-reporter": "~7.0.0",
"jschannel": "^1.0.2",
"karma": "~6.3.2",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.1",
"karma-jasmine-html-reporter": "^1.5.0",
"linkifyjs": "^2.1.5",
"lodash-es": "^4.17.4",
"markdown-it": "12.2.0",
@ -106,7 +107,6 @@
"path-browserify": "^1.0.0",
"primeng": "^12.0.0-rc.1",
"process": "^0.11.10",
"protractor": "~7.0.0",
"purify-css": "^1.2.5",
"raw-loader": "^4.0.2",
"rxjs": "^7.3.0",
@ -129,6 +129,8 @@
"videojs-dock": "^2.0.2",
"videojs-hotkeys": "^0.2.27",
"videostream": "~3.2.1",
"wdio-chromedriver-service": "^7.2.0",
"wdio-geckodriver-service": "^2.0.3",
"webpack-bundle-analyzer": "^4.4.2",
"webpack-cli": "^4.7.0",
"webtorrent": "^1.3.8",

File diff suppressed because it is too large Load Diff

View File

@ -4,12 +4,6 @@ set -eu
npm run clean:server:test
(
cd client
npm run webdriver-manager -- update
npm run webpack -- --config webpack/webpack.video-embed.js --mode development
)
npm run concurrently -- -k -s first \
"cd client && npm run ng -- e2e --port 3333 -c local" \
"cd client/e2e && ../node_modules/.bin/wdio run ./wdio.local.conf.ts" \
"NODE_ENV=test NODE_APP_INSTANCE=1 NODE_CONFIG='{ \"log\": { \"level\": \"warn\" }, \"signup\": { \"enabled\": false } }' node dist/server"

View File

@ -197,9 +197,6 @@ async function addVideo (options: {
}, sequelizeOptions)
}
// Channel has a new content, set as updated
await videoCreated.VideoChannel.setAsUpdated(t)
await autoBlacklistVideoIfNeeded({
video,
user,
@ -214,6 +211,9 @@ async function addVideo (options: {
return { videoCreated }
})
// Channel has a new content, set as updated
await videoCreated.VideoChannel.setAsUpdated()
createTorrentFederate(video, videoFile)
.then(() => {
if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {

View File

@ -753,7 +753,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
return this.Actor.isOutdated()
}
setAsUpdated (transaction: Transaction) {
setAsUpdated (transaction?: Transaction) {
return setAsUpdated('videoChannel', this.id, transaction)
}
}