aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/e2e
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-08-30 16:24:25 +0200
committerChocobozzz <me@florianbigard.com>2021-09-01 15:06:46 +0200
commit3419e0e1fe8e48a08b63ca0ded31087f913eb2b6 (patch)
tree63ac7190b79194e93aec9bbfd3c336e60f469e9d /client/e2e
parent2a4c9669d2d6ac6cd4ae43544698f826ae98080f (diff)
downloadPeerTube-3419e0e1fe8e48a08b63ca0ded31087f913eb2b6.tar.gz
PeerTube-3419e0e1fe8e48a08b63ca0ded31087f913eb2b6.tar.zst
PeerTube-3419e0e1fe8e48a08b63ca0ded31087f913eb2b6.zip
Migrate to webdriverio
Diffstat (limited to 'client/e2e')
-rw-r--r--client/e2e/local-protractor.conf.js56
-rw-r--r--client/e2e/protractor.conf.js96
-rw-r--r--client/e2e/proxy.config.json37
-rw-r--r--client/e2e/src/commands/upload.ts12
-rw-r--r--client/e2e/src/po/login.po.ts24
-rw-r--r--client/e2e/src/po/my-account.ts102
-rw-r--r--client/e2e/src/po/player.po.ts52
-rw-r--r--client/e2e/src/po/video-update.po.ts12
-rw-r--r--client/e2e/src/po/video-upload.po.ts34
-rw-r--r--client/e2e/src/po/video-watch.po.ts132
-rw-r--r--client/e2e/src/types/wdio.d.ts5
-rw-r--r--client/e2e/src/utils.ts39
-rw-r--r--client/e2e/src/videos.e2e-spec.ts140
-rw-r--r--client/e2e/tsconfig.json13
-rw-r--r--client/e2e/wdio.browserstack.conf.ts114
-rw-r--r--client/e2e/wdio.local.conf.ts43
-rw-r--r--client/e2e/wdio.main.conf.ts115
17 files changed, 597 insertions, 429 deletions
diff --git a/client/e2e/local-protractor.conf.js b/client/e2e/local-protractor.conf.js
deleted file mode 100644
index d0d4ae9a3..000000000
--- a/client/e2e/local-protractor.conf.js
+++ /dev/null
@@ -1,56 +0,0 @@
1// Protractor configuration file, see link for more information
2// https://github.com/angular/protractor/blob/master/lib/config.ts
3
4const {SpecReporter} = require('jasmine-spec-reporter')
5
6exports.config = {
7 allScriptsTimeout: 25000,
8 specs: ['./src/**/*.e2e-spec.ts'],
9
10 directConnect: true,
11
12 multiCapabilities: [
13 {
14 'browserName': 'firefox',
15 'name': 'Firefox',
16 'moz:firefoxOptions': {
17 binary: '/usr/bin/firefox-developer-edition',
18 // args: ["-headless"],
19 log: {
20 "level": "info" // default is "info"
21 }
22 }
23 },
24 {
25 'browserName': 'firefox',
26 'name': 'Firefox ESR',
27 'moz:firefoxOptions': {
28 binary: '/usr/bin/firefox-esr',
29 // args: ["-headless"],
30 log: {
31 "level": "info" // default is "info"
32 }
33 }
34 },
35 {
36 'browserName': 'chrome',
37 'name': 'Chromium'
38 }
39 ],
40
41 maxSessions: 1,
42 baseUrl: 'http://localhost:3000/',
43 framework: 'jasmine',
44 jasmineNodeOpts: {
45 showColors: true,
46 defaultTimeoutInterval: 45000,
47 print: function() {}
48 },
49
50 onPrepare() {
51 require('ts-node').register({
52 project: require('path').join(__dirname, './tsconfig.json')
53 })
54 jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }))
55 }
56}
diff --git a/client/e2e/protractor.conf.js b/client/e2e/protractor.conf.js
deleted file mode 100644
index ff0ae83e7..000000000
--- a/client/e2e/protractor.conf.js
+++ /dev/null
@@ -1,96 +0,0 @@
1// Protractor configuration file, see link for more information
2// https://github.com/angular/protractor/blob/master/lib/config.ts
3
4const {SpecReporter} = require('jasmine-spec-reporter')
5
6exports.config = {
7 allScriptsTimeout: 25000,
8 specs: [ './src/**/*.e2e-spec.ts' ],
9
10 seleniumAddress: 'http://hub-cloud.browserstack.com/wd/hub',
11 commonCapabilities: {
12 'browserstack.user': process.env.BROWSERSTACK_USER,
13 'browserstack.key': process.env.BROWSERSTACK_KEY,
14 'browserstack.local': true,
15 'browserstack.console': 'verbose',
16 'browserstack.networkLogs': true,
17 'browserstack.debug': true,
18 project: 'PeerTube',
19 build: 'Main',
20 name: 'Bstack-[Protractor] Parallel Test'
21 },
22
23 multiCapabilities: [
24 {
25 browserName: 'Safari',
26 version: '11.1',
27 name: 'Safari Desktop',
28 resolution: '1280x1024'
29 },
30 {
31 browserName: 'Chrome',
32 name: 'Latest Chrome Desktop',
33 resolution: '1280x1024'
34 },
35 {
36 browserName: 'Firefox',
37 version: '68', // ESR
38 name: 'Firefox ESR Desktop',
39 resolution: '1280x1024'
40 },
41 {
42 browserName: 'Firefox',
43 name: 'Latest Firefox Desktop',
44 resolution: '1280x1024'
45 },
46 {
47 browserName: 'Edge',
48 name: 'Latest Edge Desktop',
49 resolution: '1280x1024'
50 },
51 {
52 browserName: 'Chrome',
53 device: 'Google Nexus 6',
54 real_mobile: 'true',
55 os_version: '5.0',
56 name: 'Latest Chrome Android'
57 },
58 {
59 browserName: 'Safari',
60 device: 'iPhone 8 Plus',
61 real_mobile: 'true',
62 os_version: '11',
63 name: 'Safari iPhone'
64 },
65 {
66 browserName: 'Safari',
67 device: 'iPad 7th',
68 real_mobile: 'true',
69 os_version: '13',
70 name: 'Safari iPad'
71 }
72 ],
73
74 // maxSessions: 1,
75 // BrowserStack compatible ports: https://www.browserstack.com/question/664
76 baseUrl: 'http://localhost:3333/',
77 framework: 'jasmine',
78 jasmineNodeOpts: {
79 showColors: true,
80 defaultTimeoutInterval: 45000,
81 print: function() {}
82 },
83
84 onPrepare() {
85 require('ts-node').register({
86 project: require('path').join(__dirname, './tsconfig.json')
87 })
88 jasmine.getEnv().addReporter(new SpecReporter({
89 spec: { displayStacktrace: 'raw' }
90 }))
91 }
92}
93
94exports.config.multiCapabilities.forEach(function (caps) {
95 for (var i in exports.config.commonCapabilities) caps[i] = caps[i] || exports.config.commonCapabilities[i]
96})
diff --git a/client/e2e/proxy.config.json b/client/e2e/proxy.config.json
deleted file mode 100644
index 00bda5eb7..000000000
--- a/client/e2e/proxy.config.json
+++ /dev/null
@@ -1,37 +0,0 @@
1{
2 "/api": {
3 "target": "http://localhost:9000",
4 "secure": false
5 },
6 "/plugins": {
7 "target": "http://localhost:9000",
8 "secure": false
9 },
10 "/themes": {
11 "target": "http://localhost:9000",
12 "secure": false
13 },
14 "/static": {
15 "target": "http://localhost:9000",
16 "secure": false
17 },
18 "/lazy-static": {
19 "target": "http://localhost:9000",
20 "secure": false
21 },
22 "/socket.io": {
23 "target": "ws://localhost:9000",
24 "secure": false,
25 "ws": true
26 },
27 "/!(client)**": {
28 "target": "http://localhost:3333/client/index.html",
29 "secure": false,
30 "logLevel": "debug"
31 },
32 "/!(client)**/**": {
33 "target": "http://localhost:3333/client/index.html",
34 "secure": false,
35 "logLevel": "debug"
36 }
37}
diff --git a/client/e2e/src/commands/upload.ts b/client/e2e/src/commands/upload.ts
new file mode 100644
index 000000000..05d3f8f31
--- /dev/null
+++ b/client/e2e/src/commands/upload.ts
@@ -0,0 +1,12 @@
1browser.addCommand('chooseFile', async function (this: WebdriverIO.Element, localFilePath: string) {
2 try {
3 const remoteFile = await browser.uploadFile(localFilePath)
4
5 return this.addValue(remoteFile)
6 } catch {
7 console.log('Cannot upload file, fallback to add value.')
8
9 // Firefox does not support upload file, but if we're running the test in local we don't really need it
10 return this.addValue(localFilePath)
11 }
12}, true)
diff --git a/client/e2e/src/po/login.po.ts b/client/e2e/src/po/login.po.ts
index 20412ee0d..8e3030e43 100644
--- a/client/e2e/src/po/login.po.ts
+++ b/client/e2e/src/po/login.po.ts
@@ -1,23 +1,25 @@
1import { browser, element, by } from 'protractor' 1import { go } from '../utils'
2 2
3export class LoginPage { 3export class LoginPage {
4 async loginAsRootUser () { 4 async loginAsRootUser () {
5 await browser.get('/login') 5 await go('/login')
6 6
7 await browser.executeScript(`window.localStorage.setItem('no_instance_config_warning_modal', 'true')`) 7 await browser.execute(`window.localStorage.setItem('no_instance_config_warning_modal', 'true')`)
8 await browser.executeScript(`window.localStorage.setItem('no_welcome_modal', 'true')`) 8 await browser.execute(`window.localStorage.setItem('no_welcome_modal', 'true')`)
9 9
10 element(by.css('input#username')).sendKeys('root') 10 await $('input#username').setValue('root')
11 element(by.css('input#password')).sendKeys('test1') 11 await $('input#password').setValue('test1')
12 12
13 await browser.sleep(1000) 13 await browser.pause(1000)
14 14
15 await element(by.css('form input[type=submit]')).click() 15 await $('form input[type=submit]').click()
16 16
17 expect(this.getLoggedInInfo().getText()).toContain('root') 17 await this.getLoggedInInfoElem().waitForExist()
18
19 await expect(this.getLoggedInInfoElem()).toHaveText('root')
18 } 20 }
19 21
20 private getLoggedInInfo () { 22 private getLoggedInInfoElem () {
21 return element(by.css('.logged-in-display-name')) 23 return $('.logged-in-display-name')
22 } 24 }
23} 25}
diff --git a/client/e2e/src/po/my-account.ts b/client/e2e/src/po/my-account.ts
index 9866953e9..85dc02805 100644
--- a/client/e2e/src/po/my-account.ts
+++ b/client/e2e/src/po/my-account.ts
@@ -1,85 +1,117 @@
1import { by, element, browser } from 'protractor' 1import { go } from '../utils'
2 2
3export class MyAccountPage { 3export class MyAccountPage {
4 4
5 navigateToMyVideos () { 5 navigateToMyVideos () {
6 return element(by.css('a[href="/my-library/videos"]')).click() 6 return $('a[href="/my-library/videos"]').click()
7 } 7 }
8 8
9 navigateToMyPlaylists () { 9 navigateToMyPlaylists () {
10 return element(by.css('a[href="/my-library/video-playlists"]')).click() 10 return $('a[href="/my-library/video-playlists"]').click()
11 } 11 }
12 12
13 navigateToMyHistory () { 13 navigateToMyHistory () {
14 return element(by.css('a[href="/my-library/history/videos"]')).click() 14 return $('a[href="/my-library/history/videos"]').click()
15 } 15 }
16 16
17 // My account Videos 17 // My account Videos
18 18
19 async removeVideo (name: string) { 19 async removeVideo (name: string) {
20 const container = this.getVideoElement(name) 20 const container = await this.getVideoElement(name)
21 21
22 await container.element(by.css('.dropdown-toggle')).click() 22 await container.$('.dropdown-toggle').click()
23 23
24 const dropdownMenu = container.element(by.css('.dropdown-menu .dropdown-item:nth-child(2)')) 24 const dropdownMenu = () => container.$('.dropdown-menu .dropdown-item:nth-child(2)')
25 await browser.wait(browser.ExpectedConditions.presenceOf(dropdownMenu))
26 25
27 return dropdownMenu.click() 26 await dropdownMenu().waitForDisplayed()
27 return dropdownMenu().click()
28 } 28 }
29 29
30 validRemove () { 30 validRemove () {
31 return element(by.css('input[type=submit]')).click() 31 return $('input[type=submit]').click()
32 } 32 }
33 33
34 countVideos (names: string[]) { 34 async countVideos (names: string[]) {
35 return element.all(by.css('.video')) 35 const elements = await $$('.video').filter(async e => {
36 .filter(e => { 36 const t = await e.$('.video-miniature-name').getText()
37 return e.element(by.css('.video-miniature-name')) 37
38 .getText() 38 return names.some(n => t.includes(n))
39 .then(t => names.some(n => t.includes(n))) 39 })
40 }) 40
41 .count() 41 return elements.length
42 } 42 }
43 43
44 // My account playlists 44 // My account playlists
45 45
46 getPlaylistVideosText (name: string) { 46 async getPlaylistVideosText (name: string) {
47 return this.getPlaylist(name).element(by.css('.miniature-playlist-info-overlay')).getText() 47 const elem = await this.getPlaylist(name)
48
49 return elem.$('.miniature-playlist-info-overlay').getText()
48 } 50 }
49 51
50 clickOnPlaylist (name: string) { 52 async clickOnPlaylist (name: string) {
51 return this.getPlaylist(name).element(by.css('.miniature-thumbnail')).click() 53 const elem = await this.getPlaylist(name)
54
55 return elem.$('.miniature-thumbnail').click()
52 } 56 }
53 57
54 countTotalPlaylistElements () { 58 async countTotalPlaylistElements () {
55 return element.all(by.css('my-video-playlist-element-miniature')).count() 59 await $('<my-video-playlist-element-miniature>').waitForDisplayed()
60
61 return $$('<my-video-playlist-element-miniature>').length
56 } 62 }
57 63
58 playPlaylist () { 64 playPlaylist () {
59 return element(by.css('.playlist-info .miniature-thumbnail')).click() 65 return $('.playlist-info .miniature-thumbnail').click()
60 } 66 }
61 67
62 async goOnAssociatedPlaylistEmbed () { 68 async goOnAssociatedPlaylistEmbed () {
63 let url = await browser.getCurrentUrl() 69 let url = await browser.getUrl()
64 url = url.replace('/w/p/', '/video-playlists/embed/') 70 url = url.replace('/w/p/', '/video-playlists/embed/')
65 url = url.replace(':3333', ':9001') 71 url = url.replace(':3333', ':9001')
66 72
67 return browser.get(url) 73 return go(url)
68 } 74 }
69 75
70 // My account Videos 76 // My account Videos
71 77
72 private getVideoElement (name: string) { 78 private async getVideoElement (name: string) {
73 return element.all(by.css('.video')) 79 const video = async () => {
74 .filter(e => e.element(by.css('.video-miniature-name')).getText().then(t => t.includes(name))) 80 const videos = await $$('.video').filter(async e => {
75 .first() 81 const t = await e.$('.video-miniature-name').getText()
82
83 return t.includes(name)
84 })
85
86 return videos[0]
87 }
88
89 await browser.waitUntil(async () => {
90 return (await video()).isDisplayed()
91 })
92
93 return video()
76 } 94 }
77 95
78 // My account playlists 96 // My account playlists
79 97
80 private getPlaylist (name: string) { 98 private async getPlaylist (name: string) {
81 return element.all(by.css('my-video-playlist-miniature')) 99 const playlist = () => {
82 .filter(e => e.element(by.css('.miniature-name')).getText().then(t => t.includes(name))) 100 return $$('my-video-playlist-miniature')
83 .first() 101 .filter(async e => {
102 const t = await e.$('.miniature-name').getText()
103
104 return t.includes(name)
105 })
106 .then(elems => elems[0])
107 }
108
109 await browser.waitUntil(async () => {
110 const el = await playlist()
111
112 return el?.isDisplayed()
113 })
114
115 return playlist()
84 } 116 }
85} 117}
diff --git a/client/e2e/src/po/player.po.ts b/client/e2e/src/po/player.po.ts
index cb148a003..9d6e21009 100644
--- a/client/e2e/src/po/player.po.ts
+++ b/client/e2e/src/po/player.po.ts
@@ -1,45 +1,54 @@
1import { browser, by, element } from 'protractor'
2import { browserSleep, isIOS, isMobileDevice, isSafari } from '../utils' 1import { browserSleep, isIOS, isMobileDevice, isSafari } from '../utils'
3 2
4export class PlayerPage { 3export class PlayerPage {
5 4
6 async getWatchVideoPlayerCurrentTime () { 5 getWatchVideoPlayerCurrentTime () {
7 const elem = element(by.css('video')) 6 const elem = $('video')
8 7
9 return elem.getAttribute('currentTime') 8 if (isIOS()) {
10 } 9 return elem.getAttribute('currentTime')
10 .then(t => parseInt(t, 10))
11 .then(t => Math.round(t))
12 }
11 13
12 waitUntilPlaylistInfo (text: string) { 14 return elem.getProperty('currentTime')
13 const elem = element(by.css('.video-js .vjs-playlist-info')) 15 }
14 16
15 return browser.wait(browser.ExpectedConditions.textToBePresentInElement(elem, text)) 17 waitUntilPlaylistInfo (text: string, maxTime: number) {
18 return browser.waitUntil(async () => {
19 return (await $('.video-js .vjs-playlist-info').getText()).includes(text)
20 }, { timeout: maxTime })
16 } 21 }
17 22
18 waitUntilPlayerWrapper () { 23 waitUntilPlayerWrapper () {
19 const elem = element(by.css('#placeholder-preview')) 24 return browser.waitUntil(async () => {
20 25 return !!(await $('#placeholder-preview'))
21 return browser.wait(browser.ExpectedConditions.presenceOf(elem)) 26 })
22 } 27 }
23 28
24 async playAndPauseVideo (isAutoplay: boolean) { 29 async playAndPauseVideo (isAutoplay: boolean) {
25 const videojsEl = element(by.css('div.video-js')) 30 const videojsElem = () => $('div.video-js')
26 await browser.wait(browser.ExpectedConditions.elementToBeClickable(videojsEl)) 31
32 await videojsElem().waitForExist()
27 33
28 // Autoplay is disabled on iOS and Safari 34 // Autoplay is disabled on iOS and Safari
29 if (await isIOS() || await isSafari() || await isMobileDevice()) { 35 if (isIOS() || isSafari() || isMobileDevice()) {
30 // We can't play the video using protractor if it is not muted 36 // We can't play the video using protractor if it is not muted
31 await browser.executeScript(`document.querySelector('video').muted = true`) 37 await browser.execute(`document.querySelector('video').muted = true`)
32 await this.clickOnPlayButton() 38 await this.clickOnPlayButton()
33 } else if (isAutoplay === false) { 39 } else if (isAutoplay === false) {
34 await this.clickOnPlayButton() 40 await this.clickOnPlayButton()
35 } 41 }
36 42
37 await browserSleep(2000) 43 await browserSleep(2000)
38 await browser.wait(browser.ExpectedConditions.invisibilityOf(element(by.css('.vjs-loading-spinner'))))
39 44
40 await browserSleep(2000) 45 await browser.waitUntil(async () => {
46 return !await $('.vjs-loading-spinner').isDisplayedInViewport()
47 }, { timeout: 20 * 1000 })
41 48
42 await videojsEl.click() 49 await browserSleep(4000)
50
51 await videojsElem().click()
43 } 52 }
44 53
45 async playVideo () { 54 async playVideo () {
@@ -47,8 +56,9 @@ export class PlayerPage {
47 } 56 }
48 57
49 private async clickOnPlayButton () { 58 private async clickOnPlayButton () {
50 const playButton = element(by.css('.vjs-big-play-button')) 59 const playButton = () => $('.vjs-big-play-button')
51 await browser.wait(browser.ExpectedConditions.elementToBeClickable(playButton)) 60
52 await playButton.click() 61 await playButton().waitForClickable()
62 await playButton().click()
53 } 63 }
54} 64}
diff --git a/client/e2e/src/po/video-update.po.ts b/client/e2e/src/po/video-update.po.ts
index 752741378..3ffb7ba57 100644
--- a/client/e2e/src/po/video-update.po.ts
+++ b/client/e2e/src/po/video-update.po.ts
@@ -1,11 +1,11 @@
1import { by, element } from 'protractor'
2
3export class VideoUpdatePage { 1export class VideoUpdatePage {
4 2
5 async updateName (videoName: string) { 3 async updateName (videoName: string) {
6 const nameInput = element(by.css('input#name')) 4 const nameInput = $('input#name')
7 await nameInput.clear() 5
8 await nameInput.sendKeys(videoName) 6 await nameInput.waitForDisplayed()
7 await nameInput.clearValue()
8 await nameInput.setValue(videoName)
9 } 9 }
10 10
11 async validUpdate () { 11 async validUpdate () {
@@ -15,6 +15,6 @@ export class VideoUpdatePage {
15 } 15 }
16 16
17 private getSubmitButton () { 17 private getSubmitButton () {
18 return element(by.css('.submit-container .action-button')) 18 return $('.submit-container .action-button')
19 } 19 }
20} 20}
diff --git a/client/e2e/src/po/video-upload.po.ts b/client/e2e/src/po/video-upload.po.ts
index a248912ed..34f916b55 100644
--- a/client/e2e/src/po/video-upload.po.ts
+++ b/client/e2e/src/po/video-upload.po.ts
@@ -1,33 +1,29 @@
1import { browser, by, element } from 'protractor'
2import { FileDetector } from 'selenium-webdriver/remote'
3import { join } from 'path' 1import { join } from 'path'
4 2
5export class VideoUploadPage { 3export class VideoUploadPage {
6 async navigateTo () { 4 async navigateTo () {
7 await element(by.css('.header .publish-button')).click() 5 await $('.header .publish-button').click()
8 6
9 return browser.wait(browser.ExpectedConditions.visibilityOf(element(by.css('.upload-video-container')))) 7 await $('.upload-video-container').waitForDisplayed()
10 } 8 }
11 9
12 async uploadVideo () { 10 async uploadVideo () {
13 browser.setFileDetector(new FileDetector())
14
15 const fileToUpload = join(__dirname, '../../fixtures/video.mp4') 11 const fileToUpload = join(__dirname, '../../fixtures/video.mp4')
16 const fileInputSelector = '.upload-video-container input[type=file]' 12 const fileInputSelector = '.upload-video-container input[type=file]'
17 const parentFileInput = '.upload-video-container .button-file' 13 const parentFileInput = '.upload-video-container .button-file'
18 14
19 // Avoid sending keys on non visible element 15 // Avoid sending keys on non visible element
20 await browser.executeScript(`document.querySelector('${fileInputSelector}').style.opacity = 1`) 16 await browser.execute(`document.querySelector('${fileInputSelector}').style.opacity = 1`)
21 await browser.executeScript(`document.querySelector('${parentFileInput}').style.overflow = 'initial'`) 17 await browser.execute(`document.querySelector('${parentFileInput}').style.overflow = 'initial'`)
22 18
23 await browser.sleep(1000) 19 await browser.pause(1000)
24 20
25 const elem = element(by.css(fileInputSelector)) 21 const elem = await $(fileInputSelector)
26 await elem.sendKeys(fileToUpload) 22 await elem.chooseFile(fileToUpload)
27 23
28 // Wait for the upload to finish 24 // Wait for the upload to finish
29 await browser.wait(async () => { 25 await browser.waitUntil(async () => {
30 const actionButton = this.getSecondStepSubmitButton().element(by.css('.action-button')) 26 const actionButton = this.getSecondStepSubmitButton().$('.action-button')
31 27
32 const klass = await actionButton.getAttribute('class') 28 const klass = await actionButton.getAttribute('class')
33 return !klass.includes('disabled') 29 return !klass.includes('disabled')
@@ -35,16 +31,18 @@ export class VideoUploadPage {
35 } 31 }
36 32
37 async validSecondUploadStep (videoName: string) { 33 async validSecondUploadStep (videoName: string) {
38 const nameInput = element(by.css('input#name')) 34 const nameInput = $('input#name')
39 await nameInput.clear() 35 await nameInput.clearValue()
40 await nameInput.sendKeys(videoName) 36 await nameInput.setValue(videoName)
41 37
42 await this.getSecondStepSubmitButton().click() 38 await this.getSecondStepSubmitButton().click()
43 39
44 return browser.wait(browser.ExpectedConditions.urlContains('/w/')) 40 return browser.waitUntil(async () => {
41 return (await browser.getUrl()).includes('/w/')
42 })
45 } 43 }
46 44
47 private getSecondStepSubmitButton () { 45 private getSecondStepSubmitButton () {
48 return element(by.css('.submit-container my-button')) 46 return $('.submit-container my-button')
49 } 47 }
50} 48}
diff --git a/client/e2e/src/po/video-watch.po.ts b/client/e2e/src/po/video-watch.po.ts
index c875d5854..01061d5d4 100644
--- a/client/e2e/src/po/video-watch.po.ts
+++ b/client/e2e/src/po/video-watch.po.ts
@@ -1,5 +1,4 @@
1import { browser, by, element, ElementFinder, ExpectedConditions } from 'protractor' 1import { browserSleep, go } from '../utils'
2import { browserSleep, isMobileDevice } from '../utils'
3 2
4export class VideoWatchPage { 3export class VideoWatchPage {
5 async goOnVideosList (isMobileDevice: boolean, isSafari: boolean) { 4 async goOnVideosList (isMobileDevice: boolean, isSafari: boolean) {
@@ -12,19 +11,19 @@ export class VideoWatchPage {
12 url = '/videos/recently-added' 11 url = '/videos/recently-added'
13 } 12 }
14 13
15 await browser.get(url, 20000) 14 await go(url)
16 15
17 // Waiting the following element does not work on Safari... 16 // Waiting the following element does not work on Safari...
18 if (isSafari) return browserSleep(3000) 17 if (isSafari) return browserSleep(3000)
19 18
20 const elem = element.all(by.css('.videos .video-miniature .video-miniature-name')).first() 19 await $('.videos .video-miniature .video-miniature-name').waitForDisplayed()
21 return browser.wait(browser.ExpectedConditions.visibilityOf(elem))
22 } 20 }
23 21
24 getVideosListName () { 22 async getVideosListName () {
25 return element.all(by.css('.videos .video-miniature .video-miniature-name')) 23 const elems = await $$('.videos .video-miniature .video-miniature-name')
26 .getText() 24 const texts = await Promise.all(elems.map(e => e.getText()))
27 .then((texts: any) => texts.map((t: any) => t.trim())) 25
26 return texts.map(t => t.trim())
28 } 27 }
29 28
30 waitWatchVideoName (videoName: string, isMobileDevice: boolean, isSafari: boolean) { 29 waitWatchVideoName (videoName: string, isMobileDevice: boolean, isSafari: boolean) {
@@ -33,99 +32,134 @@ export class VideoWatchPage {
33 // On mobile we display the first node, on desktop the second 32 // On mobile we display the first node, on desktop the second
34 const index = isMobileDevice ? 0 : 1 33 const index = isMobileDevice ? 0 : 1
35 34
36 const elem = element.all(by.css('.video-info .video-info-name')).get(index) 35 return browser.waitUntil(async () => {
37 return browser.wait(browser.ExpectedConditions.textToBePresentInElement(elem, videoName)) 36 return (await $$('.video-info .video-info-name')[index].getText()).includes(videoName)
37 })
38 } 38 }
39 39
40 getVideoName () { 40 getVideoName () {
41 return this.getVideoNameElement().getText() 41 return this.getVideoNameElement().then(e => e.getText())
42 } 42 }
43 43
44 async goOnAssociatedEmbed () { 44 async goOnAssociatedEmbed () {
45 let url = await browser.getCurrentUrl() 45 let url = await browser.getUrl()
46 url = url.replace('/w/', '/videos/embed/') 46 url = url.replace('/w/', '/videos/embed/')
47 url = url.replace(':3333', ':9001') 47 url = url.replace(':3333', ':9001')
48 48
49 return browser.get(url) 49 return go(url)
50 } 50 }
51 51
52 async goOnP2PMediaLoaderEmbed () { 52 goOnP2PMediaLoaderEmbed () {
53 return browser.get('https://peertube2.cpy.re/videos/embed/969bf103-7818-43b5-94a0-de159e13de50') 53 return go(
54 'https://peertube2.cpy.re/videos/embed/969bf103-7818-43b5-94a0-de159e13de50'
55 )
54 } 56 }
55 57
56 async goOnP2PMediaLoaderPlaylistEmbed () { 58 goOnP2PMediaLoaderPlaylistEmbed () {
57 return browser.get('https://peertube2.cpy.re/video-playlists/embed/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a') 59 return go(
60 'https://peertube2.cpy.re/video-playlists/embed/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a'
61 )
58 } 62 }
59 63
60 async clickOnVideo (videoName: string) { 64 async clickOnVideo (videoName: string) {
61 const video = element.all(by.css('.videos .video-miniature .video-miniature-name')) 65 const video = async () => {
62 .filter(e => e.getText().then(t => t === videoName )) 66 const videos = await $$('.videos .video-miniature .video-miniature-name').filter(async e => {
63 .first() 67 const t = await e.getText()
68
69 return t === videoName
70 })
71
72 return videos[0]
73 }
74
75 await browser.waitUntil(async () => {
76 const elem = await video()
64 77
65 await browser.wait(browser.ExpectedConditions.elementToBeClickable(video)) 78 return elem?.isClickable()
66 await video.click() 79 });
67 80
68 await browser.wait(browser.ExpectedConditions.urlContains('/w/')) 81 (await video()).click()
82
83 await browser.waitUntil(async () => (await browser.getUrl()).includes('/w/'))
69 } 84 }
70 85
71 async clickOnFirstVideo () { 86 async clickOnFirstVideo () {
72 const video = element.all(by.css('.videos .video-miniature .video-thumbnail')).first() 87 const video = () => $('.videos .video-miniature .video-thumbnail')
73 const videoName = element.all(by.css('.videos .video-miniature .video-miniature-name')).first() 88 const videoName = () => $('.videos .video-miniature .video-miniature-name')
89
90 await video().waitForClickable()
74 91
75 // Don't know why but the expectation fails on Safari 92 const textToReturn = await videoName().getText()
76 await browser.wait(browser.ExpectedConditions.elementToBeClickable(video)) 93 await video().click()
77 94
78 const textToReturn = videoName.getText() 95 await browser.waitUntil(async () => (await browser.getUrl()).includes('/w/'))
79 await video.click()
80 96
81 await browser.wait(browser.ExpectedConditions.urlContains('/w/'))
82 return textToReturn 97 return textToReturn
83 } 98 }
84 99
85 async clickOnUpdate () { 100 async clickOnUpdate () {
86 const dropdown = element(by.css('my-video-actions-dropdown .action-button')) 101 const dropdown = $('my-video-actions-dropdown .action-button')
87 await dropdown.click() 102 await dropdown.click()
88 103
89 const items: ElementFinder[] = await element.all(by.css('.dropdown-menu.show .dropdown-item')) 104 await $('.dropdown-menu.show .dropdown-item').waitForDisplayed()
105 const items = await $$('.dropdown-menu.show .dropdown-item')
90 106
91 for (const item of items) { 107 for (const item of items) {
92 const href = await item.getAttribute('href') 108 const href = await item.getAttribute('href')
93 109
94 if (href && href.includes('/update/')) { 110 if (href?.includes('/update/')) {
95 await item.click() 111 await item.click()
96 return 112 return
97 } 113 }
98 } 114 }
99 } 115 }
100 116
101 async clickOnSave () { 117 clickOnSave () {
102 return element(by.css('.action-button-save')).click() 118 return $('.action-button-save').click()
103 } 119 }
104 120
105 async createPlaylist (name: string) { 121 async createPlaylist (name: string) {
106 await element(by.css('.new-playlist-button')).click() 122 const newPlaylistButton = () => $('.new-playlist-button')
123
124 await newPlaylistButton().waitForClickable()
125 await newPlaylistButton().click()
126
127 const displayName = () => $('#displayName')
107 128
108 await element(by.css('#displayName')).sendKeys(name) 129 await displayName().waitForDisplayed()
130 await displayName().setValue(name)
109 131
110 return element(by.css('.new-playlist-block input[type=submit]')).click() 132 return $('.new-playlist-block input[type=submit]').click()
111 } 133 }
112 134
113 async saveToPlaylist (name: string) { 135 async saveToPlaylist (name: string) {
114 return element.all(by.css('my-video-add-to-playlist .playlist')) 136 const playlist = () => $('my-video-add-to-playlist').$(`.playlist=${name}`)
115 .filter(p => p.getText().then(t => t === name)) 137
116 .click() 138 await playlist().waitForDisplayed()
139
140 return playlist().click()
117 } 141 }
118 142
119 waitUntilVideoName (name: string, maxTime: number) { 143 waitUntilVideoName (name: string, maxTime: number) {
120 const elem = this.getVideoNameElement() 144 return browser.waitUntil(async () => {
121 145 return (await this.getVideoName()) === name
122 return browser.wait(ExpectedConditions.textToBePresentInElement(elem, name), maxTime) 146 }, { timeout: maxTime })
123 } 147 }
124 148
125 private getVideoNameElement () { 149 private async getVideoNameElement () {
126 // We have 2 video info name block, pick the first that is not empty 150 // We have 2 video info name block, pick the first that is not empty
127 return element.all(by.css('.video-info-first-row .video-info-name')) 151 const elem = async () => {
128 .filter(e => e.getText().then(t => !!t)) 152 const elems = await $$('.video-info-first-row .video-info-name').filter(e => e.isDisplayed())
129 .first() 153
154 return elems[0]
155 }
156
157 await browser.waitUntil(async () => {
158 const e = await elem()
159
160 return e?.isDisplayed()
161 })
162
163 return elem()
130 } 164 }
131} 165}
diff --git a/client/e2e/src/types/wdio.d.ts b/client/e2e/src/types/wdio.d.ts
new file mode 100644
index 000000000..6dc2cbd87
--- /dev/null
+++ b/client/e2e/src/types/wdio.d.ts
@@ -0,0 +1,5 @@
1declare namespace WebdriverIO {
2 interface Element {
3 chooseFile: (path: string) => Promise<void>
4 }
5}
diff --git a/client/e2e/src/utils.ts b/client/e2e/src/utils.ts
index a9d30c03a..df1c29238 100644
--- a/client/e2e/src/utils.ts
+++ b/client/e2e/src/utils.ts
@@ -1,34 +1,37 @@
1import { browser } from 'protractor'
2
3async function browserSleep (amount: number) { 1async function browserSleep (amount: number) {
4 const oldValue = await browser.waitForAngularEnabled() 2 await browser.pause(amount)
5 3}
6 // iOS does not seem to work with protractor
7 // https://github.com/angular/protractor/issues/2840
8 if (await isIOS()) browser.waitForAngularEnabled(true)
9 4
10 await browser.sleep(amount) 5function isMobileDevice () {
6 const platformName = (browser.capabilities['platformName'] || '').toLowerCase()
11 7
12 if (await isIOS()) browser.waitForAngularEnabled(oldValue) 8 return platformName === 'android' || platformName === 'ios'
13} 9}
14 10
15async function isMobileDevice () { 11function isSafari () {
16 const caps = await browser.getCapabilities() 12 return browser.capabilities['browserName'] &&
17 return caps.get('realMobile') === 'true' || caps.get('realMobile') === true 13 browser.capabilities['browserName'].toLowerCase() === 'safari'
18} 14}
19 15
20async function isSafari () { 16function isIOS () {
21 const caps = await browser.getCapabilities() 17 return isMobileDevice() && isSafari()
22 return caps.get('browserName') && caps.get('browserName').toLowerCase() === 'safari'
23} 18}
24 19
25async function isIOS () { 20async function go (url: string) {
26 return await isMobileDevice() && await isSafari() 21 await browser.url(url)
22
23 // Hide notifications that could fail tests when hiding buttons
24 await browser.execute(() => {
25 const style = document.createElement('style')
26 style.innerHTML = 'p-toast { display: none }'
27 document.head.appendChild(style)
28 })
27} 29}
28 30
29export { 31export {
30 isMobileDevice, 32 isMobileDevice,
31 isSafari, 33 isSafari,
32 isIOS, 34 isIOS,
35 go,
33 browserSleep 36 browserSleep
34} 37}
diff --git a/client/e2e/src/videos.e2e-spec.ts b/client/e2e/src/videos.e2e-spec.ts
index fc816d1bf..7f7c814e1 100644
--- a/client/e2e/src/videos.e2e-spec.ts
+++ b/client/e2e/src/videos.e2e-spec.ts
@@ -1,14 +1,13 @@
1import { browser } from 'protractor'
2import { LoginPage } from './po/login.po' 1import { LoginPage } from './po/login.po'
3import { MyAccountPage } from './po/my-account' 2import { MyAccountPage } from './po/my-account'
4import { PlayerPage } from './po/player.po' 3import { PlayerPage } from './po/player.po'
5import { VideoUpdatePage } from './po/video-update.po' 4import { VideoUpdatePage } from './po/video-update.po'
6import { VideoUploadPage } from './po/video-upload.po' 5import { VideoUploadPage } from './po/video-upload.po'
7import { VideoWatchPage } from './po/video-watch.po' 6import { VideoWatchPage } from './po/video-watch.po'
8import { isIOS, isMobileDevice, isSafari } from './utils' 7import { browserSleep, go, isIOS, isMobileDevice, isSafari } from './utils'
9 8
10async function skipIfUploadNotSupported () { 9function isUploadUnsupported () {
11 if (await isMobileDevice() || await isSafari()) { 10 if (isMobileDevice() || isSafari()) {
12 console.log('Skipping because we are on a real device or Safari and BrowserStack does not support file upload.') 11 console.log('Skipping because we are on a real device or Safari and BrowserStack does not support file upload.')
13 return true 12 return true
14 } 13 }
@@ -24,11 +23,30 @@ describe('Videos workflow', () => {
24 let loginPage: LoginPage 23 let loginPage: LoginPage
25 let playerPage: PlayerPage 24 let playerPage: PlayerPage
26 25
27 let videoName = new Date().getTime() + ' video' 26 let videoName = Math.random() + ' video'
28 const video2Name = new Date().getTime() + ' second video' 27 const video2Name = Math.random() + ' second video'
29 const playlistName = new Date().getTime() + ' playlist' 28 const playlistName = Math.random() + ' playlist'
30 let videoWatchUrl: string 29 let videoWatchUrl: string
31 30
31 before(async () => {
32 if (isIOS()) {
33 console.log('iOS detected')
34 } else if (isMobileDevice()) {
35 console.log('Android detected.')
36 } else if (isSafari()) {
37 console.log('Safari detected.')
38 }
39
40 if (isUploadUnsupported()) return
41
42 await browser.waitUntil(async () => {
43 await go('/')
44 await browserSleep(500)
45
46 return $('<my-app>').isDisplayed()
47 }, { timeout: 20 * 1000 })
48 })
49
32 beforeEach(async () => { 50 beforeEach(async () => {
33 videoWatchPage = new VideoWatchPage() 51 videoWatchPage = new VideoWatchPage()
34 videoUploadPage = new VideoUploadPage() 52 videoUploadPage = new VideoUploadPage()
@@ -37,25 +55,13 @@ describe('Videos workflow', () => {
37 loginPage = new LoginPage() 55 loginPage = new LoginPage()
38 playerPage = new PlayerPage() 56 playerPage = new PlayerPage()
39 57
40 if (await isIOS()) { 58 if (!isMobileDevice()) {
41 // iOS does not seem to work with protractor 59 await browser.maximizeWindow()
42 // https://github.com/angular/protractor/issues/2840
43 browser.waitForAngularEnabled(false)
44
45 console.log('iOS detected')
46 } else if (await isMobileDevice()) {
47 console.log('Android detected.')
48 } else if (await isSafari()) {
49 console.log('Safari detected.')
50 }
51
52 if (!await isMobileDevice()) {
53 await browser.driver.manage().window().maximize()
54 } 60 }
55 }) 61 })
56 62
57 it('Should log in', async () => { 63 it('Should log in', async () => {
58 if (await isMobileDevice() || await isSafari()) { 64 if (isMobileDevice() || isSafari()) {
59 console.log('Skipping because we are on a real device or Safari and BrowserStack does not support file upload.') 65 console.log('Skipping because we are on a real device or Safari and BrowserStack does not support file upload.')
60 return 66 return
61 } 67 }
@@ -64,7 +70,7 @@ describe('Videos workflow', () => {
64 }) 70 })
65 71
66 it('Should upload a video', async () => { 72 it('Should upload a video', async () => {
67 if (await skipIfUploadNotSupported()) return 73 if (isUploadUnsupported()) return
68 74
69 await videoUploadPage.navigateTo() 75 await videoUploadPage.navigateTo()
70 76
@@ -73,62 +79,52 @@ describe('Videos workflow', () => {
73 }) 79 })
74 80
75 it('Should list videos', async () => { 81 it('Should list videos', async () => {
76 await videoWatchPage.goOnVideosList(await isMobileDevice(), await isSafari()) 82 await videoWatchPage.goOnVideosList(isMobileDevice(), isSafari())
77 83
78 if (await skipIfUploadNotSupported()) return 84 if (isUploadUnsupported()) return
79 85
80 const videoNames = videoWatchPage.getVideosListName() 86 const videoNames = await videoWatchPage.getVideosListName()
81 expect(videoNames).toContain(videoName) 87 expect(videoNames).toContain(videoName)
82 }) 88 })
83 89
84 it('Should go on video watch page', async () => { 90 it('Should go on video watch page', async () => {
85 let videoNameToExcept = videoName 91 let videoNameToExcept = videoName
86 92
87 if (await isMobileDevice() || await isSafari()) { 93 if (isMobileDevice() || isSafari()) {
88 await browser.get('https://peertube2.cpy.re/w/122d093a-1ede-43bd-bd34-59d2931ffc5e') 94 await go('https://peertube2.cpy.re/w/122d093a-1ede-43bd-bd34-59d2931ffc5e')
89 videoNameToExcept = 'E2E tests' 95 videoNameToExcept = 'E2E tests'
90 } else { 96 } else {
91 await videoWatchPage.clickOnVideo(videoName) 97 await videoWatchPage.clickOnVideo(videoName)
92 } 98 }
93 99
94 return videoWatchPage.waitWatchVideoName(videoNameToExcept, await isMobileDevice(), await isSafari()) 100 return videoWatchPage.waitWatchVideoName(videoNameToExcept, isMobileDevice(), isSafari())
95 }) 101 })
96 102
97 it('Should play the video', async () => { 103 it('Should play the video', async () => {
98 videoWatchUrl = await browser.getCurrentUrl() 104 videoWatchUrl = await browser.getUrl()
99 105
100 await playerPage.playAndPauseVideo(true) 106 await playerPage.playAndPauseVideo(true)
101 expect(playerPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2) 107 expect(await playerPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2)
102 }) 108 })
103 109
104 it('Should watch the associated embed video', async () => { 110 it('Should watch the associated embed video', async () => {
105 const oldValue = await browser.waitForAngularEnabled()
106 await browser.waitForAngularEnabled(false)
107
108 await videoWatchPage.goOnAssociatedEmbed() 111 await videoWatchPage.goOnAssociatedEmbed()
109 112
110 await playerPage.playAndPauseVideo(false) 113 await playerPage.playAndPauseVideo(false)
111 expect(playerPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2) 114 expect(await playerPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2)
112
113 await browser.waitForAngularEnabled(oldValue)
114 }) 115 })
115 116
116 it('Should watch the p2p media loader embed video', async () => { 117 it('Should watch the p2p media loader embed video', async () => {
117 const oldValue = await browser.waitForAngularEnabled()
118 await browser.waitForAngularEnabled(false)
119
120 await videoWatchPage.goOnP2PMediaLoaderEmbed() 118 await videoWatchPage.goOnP2PMediaLoaderEmbed()
121 119
122 await playerPage.playAndPauseVideo(false) 120 await playerPage.playAndPauseVideo(false)
123 expect(playerPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2) 121 expect(await playerPage.getWatchVideoPlayerCurrentTime()).toBeGreaterThanOrEqual(2)
124
125 await browser.waitForAngularEnabled(oldValue)
126 }) 122 })
127 123
128 it('Should update the video', async () => { 124 it('Should update the video', async () => {
129 if (await skipIfUploadNotSupported()) return 125 if (isUploadUnsupported()) return
130 126
131 await browser.get(videoWatchUrl) 127 await go(videoWatchUrl)
132 128
133 await videoWatchPage.clickOnUpdate() 129 await videoWatchPage.clickOnUpdate()
134 130
@@ -142,14 +138,14 @@ describe('Videos workflow', () => {
142 }) 138 })
143 139
144 it('Should add the video in my playlist', async () => { 140 it('Should add the video in my playlist', async () => {
145 if (await skipIfUploadNotSupported()) return 141 if (isUploadUnsupported()) return
146 142
147 await videoWatchPage.clickOnSave() 143 await videoWatchPage.clickOnSave()
148 144
149 await videoWatchPage.createPlaylist(playlistName) 145 await videoWatchPage.createPlaylist(playlistName)
150 146
151 await videoWatchPage.saveToPlaylist(playlistName) 147 await videoWatchPage.saveToPlaylist(playlistName)
152 await browser.sleep(5000) 148 await browser.pause(5000)
153 149
154 await videoUploadPage.navigateTo() 150 await videoUploadPage.navigateTo()
155 151
@@ -161,7 +157,7 @@ describe('Videos workflow', () => {
161 }) 157 })
162 158
163 it('Should have the playlist in my account', async () => { 159 it('Should have the playlist in my account', async () => {
164 if (await skipIfUploadNotSupported()) return 160 if (isUploadUnsupported()) return
165 161
166 await myAccountPage.navigateToMyPlaylists() 162 await myAccountPage.navigateToMyPlaylists()
167 163
@@ -175,26 +171,18 @@ describe('Videos workflow', () => {
175 }) 171 })
176 172
177 it('Should watch the playlist', async () => { 173 it('Should watch the playlist', async () => {
178 if (await skipIfUploadNotSupported()) return 174 if (isUploadUnsupported()) return
179 175
180 await myAccountPage.playPlaylist() 176 await myAccountPage.playPlaylist()
181 177
182 const oldValue = await browser.waitForAngularEnabled() 178 await videoWatchPage.waitUntilVideoName(video2Name, 30 * 1000)
183 await browser.waitForAngularEnabled(false)
184
185 await videoWatchPage.waitUntilVideoName(video2Name, 20000 * 1000)
186
187 await browser.waitForAngularEnabled(oldValue)
188 }) 179 })
189 180
190 it('Should watch the webtorrent playlist in the embed', async () => { 181 it('Should watch the webtorrent playlist in the embed', async () => {
191 if (await skipIfUploadNotSupported()) return 182 if (isUploadUnsupported()) return
192
193 const accessToken = await browser.executeScript(`return window.localStorage.getItem('access_token');`)
194 const refreshToken = await browser.executeScript(`return window.localStorage.getItem('refresh_token');`)
195 183
196 const oldValue = await browser.waitForAngularEnabled() 184 const accessToken = await browser.execute(`return window.localStorage.getItem('access_token');`)
197 await browser.waitForAngularEnabled(false) 185 const refreshToken = await browser.execute(`return window.localStorage.getItem('refresh_token');`)
198 186
199 await myAccountPage.goOnAssociatedPlaylistEmbed() 187 await myAccountPage.goOnAssociatedPlaylistEmbed()
200 188
@@ -202,49 +190,45 @@ describe('Videos workflow', () => {
202 190
203 console.log('Will set %s and %s tokens in local storage.', accessToken, refreshToken) 191 console.log('Will set %s and %s tokens in local storage.', accessToken, refreshToken)
204 192
205 await browser.executeScript(`window.localStorage.setItem('access_token', '${accessToken}');`) 193 await browser.execute(`window.localStorage.setItem('access_token', '${accessToken}');`)
206 await browser.executeScript(`window.localStorage.setItem('refresh_token', '${refreshToken}');`) 194 await browser.execute(`window.localStorage.setItem('refresh_token', '${refreshToken}');`)
207 await browser.executeScript(`window.localStorage.setItem('token_type', 'Bearer');`) 195 await browser.execute(`window.localStorage.setItem('token_type', 'Bearer');`)
208 196
209 await browser.refresh() 197 await browser.refresh()
210 198
211 await playerPage.playVideo() 199 await playerPage.playVideo()
212 200
213 await playerPage.waitUntilPlaylistInfo('2/2') 201 await playerPage.waitUntilPlaylistInfo('2/2', 30 * 1000)
214
215 await browser.waitForAngularEnabled(oldValue)
216 }) 202 })
217 203
218 it('Should watch the HLS playlist in the embed', async () => { 204 it('Should watch the HLS playlist in the embed', async () => {
219 const oldValue = await browser.waitForAngularEnabled()
220 await browser.waitForAngularEnabled(false)
221
222 await videoWatchPage.goOnP2PMediaLoaderPlaylistEmbed() 205 await videoWatchPage.goOnP2PMediaLoaderPlaylistEmbed()
223 206
224 await playerPage.playVideo() 207 await playerPage.playVideo()
225 208
226 await playerPage.waitUntilPlaylistInfo('2/2') 209 await playerPage.waitUntilPlaylistInfo('2/2', 30 * 1000)
227
228 await browser.waitForAngularEnabled(oldValue)
229 }) 210 })
230 211
231 it('Should delete the video 2', async () => { 212 it('Should delete the video 2', async () => {
232 if (await skipIfUploadNotSupported()) return 213 if (isUploadUnsupported()) return
233 214
234 // Go to the dev website 215 // Go to the dev website
235 await browser.get(videoWatchUrl) 216 await go(videoWatchUrl)
236 217
237 await myAccountPage.navigateToMyVideos() 218 await myAccountPage.navigateToMyVideos()
238 219
239 await myAccountPage.removeVideo(video2Name) 220 await myAccountPage.removeVideo(video2Name)
240 await myAccountPage.validRemove() 221 await myAccountPage.validRemove()
241 222
242 const count = await myAccountPage.countVideos([ videoName, video2Name ]) 223 await browser.waitUntil(async () => {
243 expect(count).toEqual(1) 224 const count = await myAccountPage.countVideos([ videoName, video2Name ])
225
226 return count === 1
227 })
244 }) 228 })
245 229
246 it('Should delete the first video', async () => { 230 it('Should delete the first video', async () => {
247 if (await skipIfUploadNotSupported()) return 231 if (isUploadUnsupported()) return
248 232
249 await myAccountPage.removeVideo(videoName) 233 await myAccountPage.removeVideo(videoName)
250 await myAccountPage.validRemove() 234 await myAccountPage.validRemove()
diff --git a/client/e2e/tsconfig.json b/client/e2e/tsconfig.json
index 77d311e88..f42206621 100644
--- a/client/e2e/tsconfig.json
+++ b/client/e2e/tsconfig.json
@@ -5,9 +5,14 @@
5 "module": "commonjs", 5 "module": "commonjs",
6 "target": "es5", 6 "target": "es5",
7 "types": [ 7 "types": [
8 "jasmine", 8 "node",
9 "jasminewd2", 9 "webdriverio/async",
10 "node" 10 "@wdio/mocha-framework",
11 "expect-webdriverio"
11 ] 12 ]
12 } 13 },
14 "include": [
15 "src/**/*.ts",
16 "./*.ts"
17 ]
13} 18}
diff --git a/client/e2e/wdio.browserstack.conf.ts b/client/e2e/wdio.browserstack.conf.ts
new file mode 100644
index 000000000..af3a454fc
--- /dev/null
+++ b/client/e2e/wdio.browserstack.conf.ts
@@ -0,0 +1,114 @@
1import { config as mainConfig } from './wdio.main.conf'
2
3const user = process.env.BROWSERSTACK_USER
4const key = process.env.BROWSERSTACK_KEY
5
6if (!user) throw new Error('Miss browser stack user')
7if (!key) throw new Error('Miss browser stack key')
8
9function buildMainOptions (sessionName: string) {
10 return {
11 projectName: 'PeerTube',
12 buildName: 'Main E2E - ' + new Date().toISOString().split('T')[0],
13 sessionName,
14 consoleLogs: 'info',
15 networkLogs: true
16 }
17}
18
19function buildBStackDesktopOptions (sessionName: string, resolution?: string) {
20 return {
21 'bstack:options': {
22 ...buildMainOptions(sessionName),
23
24 resolution
25 }
26 }
27}
28
29function buildBStackMobileOptions (sessionName: string, deviceName: string, osVersion: string) {
30 return {
31 'bstack:options': {
32 ...buildMainOptions(sessionName),
33
34 realMobile: true,
35 osVersion,
36 deviceName
37 }
38 }
39}
40
41module.exports = {
42 config: {
43 ...mainConfig,
44
45 user,
46 key,
47
48 maxInstances: 5,
49
50 capabilities: [
51 {
52 browserName: 'Chrome',
53
54 ...buildBStackDesktopOptions('Latest Chrome Desktop', '1280x1024')
55 },
56 {
57 browserName: 'Firefox',
58 browserVersion: '68', // ESR
59
60 ...buildBStackDesktopOptions('Firefox ESR Desktop', '1280x1024')
61 },
62 {
63 browserName: 'Safari',
64 browserVersion: '11.1',
65
66 ...buildBStackDesktopOptions('Safari Desktop', '1280x1024')
67 },
68 {
69 browserName: 'Firefox',
70
71 ...buildBStackDesktopOptions('Firefox Latest', '1280x1024')
72 },
73 {
74 browserName: 'Edge',
75
76 ...buildBStackDesktopOptions('Edge Latest', '1280x1024')
77 },
78
79 {
80 browserName: 'Chrome',
81
82 ...buildBStackMobileOptions('Latest Chrome Android', 'Samsung Galaxy S6', '5.0')
83 },
84 {
85 browserName: 'Safari',
86
87 ...buildBStackMobileOptions('Safari iPhone', 'iPhone 8 Plus', '11')
88 },
89 {
90 browserName: 'Safari',
91
92 ...buildBStackMobileOptions('Safari iPad', 'iPad 7th', '13')
93 }
94 ],
95
96 host: 'hub-cloud.browserstack.com',
97 connectionRetryTimeout: 240000,
98 waitforTimeout: 20000,
99
100 services: [
101 [
102 'browserstack', { browserstackLocal: true }
103 ]
104 ],
105
106 after: function (result) {
107 if (result === 0) {
108 browser.executeScript('browserstack_executor: {"action": "setSessionStatus", "arguments": {"status":"passed","reason": ""}}', [])
109 } else {
110 browser.executeScript('browserstack_executor: {"action": "setSessionStatus", "arguments": {"status":"failed","reason": ""}}', [])
111 }
112 }
113 } as WebdriverIO.Config
114}
diff --git a/client/e2e/wdio.local.conf.ts b/client/e2e/wdio.local.conf.ts
new file mode 100644
index 000000000..4071aa249
--- /dev/null
+++ b/client/e2e/wdio.local.conf.ts
@@ -0,0 +1,43 @@
1import { config as mainConfig } from './wdio.main.conf'
2
3const prefs = {
4 'intl.accept_languages': 'en'
5}
6
7module.exports = {
8 config: {
9 ...mainConfig,
10
11 runner: 'local',
12
13 maxInstances: 1,
14
15 capabilities: [
16 {
17 browserName: 'chrome',
18 acceptInsecureCerts: true,
19 'goog:chromeOptions': {
20 prefs
21 }
22 },
23 {
24 browserName: 'firefox',
25 'moz:firefoxOptions': {
26 // args: [ '-headless' ],
27 binary: '/usr/bin/firefox-developer-edition',
28 prefs
29 }
30 },
31 {
32 browserName: 'firefox',
33 'moz:firefoxOptions': {
34 // args: [ '-headless' ],
35 binary: '/usr/bin/firefox-esr',
36 prefs
37 }
38 }
39 ],
40
41 services: [ 'chromedriver', 'geckodriver' ]
42 } as WebdriverIO.Config
43}
diff --git a/client/e2e/wdio.main.conf.ts b/client/e2e/wdio.main.conf.ts
new file mode 100644
index 000000000..e2ef99967
--- /dev/null
+++ b/client/e2e/wdio.main.conf.ts
@@ -0,0 +1,115 @@
1export const config = {
2 //
3 // ====================
4 // Runner Configuration
5 // ====================
6 //
7 //
8 // ==================
9 // Specify Test Files
10 // ==================
11 // Define which test specs should run. The pattern is relative to the directory
12 // from which `wdio` was called.
13 //
14 // The specs are defined as an array of spec files (optionally using wildcards
15 // that will be expanded). The test for each spec file will be run in a separate
16 // worker process. In order to have a group of spec files run in the same worker
17 // process simply enclose them in an array within the specs array.
18 //
19 // If you are calling `wdio` from an NPM script (see https://docs.npmjs.com/cli/run-script),
20 // then the current working directory is where your `package.json` resides, so `wdio`
21 // will be called from there.
22 //
23 specs: [
24 './src/**/*.e2e-spec.ts'
25 ],
26 // Patterns to exclude.
27 exclude: [
28 // 'path/to/excluded/files'
29 ],
30 //
31 // ===================
32 // Test Configurations
33 // ===================
34 // Define all options that are relevant for the WebdriverIO instance here
35 //
36 // Level of logging verbosity: trace | debug | info | warn | error | silent
37 logLevel: 'info',
38 //
39 // Set specific log levels per logger
40 // loggers:
41 // - webdriver, webdriverio
42 // - @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service
43 // - @wdio/mocha-framework, @wdio/jasmine-framework
44 // - @wdio/local-runner
45 // - @wdio/sumologic-reporter
46 // - @wdio/cli, @wdio/config, @wdio/utils
47 // Level of logging verbosity: trace | debug | info | warn | error | silent
48 // logLevels: {
49 // webdriver: 'info',
50 // '@wdio/appium-service': 'info'
51 // },
52 //
53 // If you only want to run your tests until a specific amount of tests have failed use
54 // bail (default is 0 - don't bail, run all tests).
55 bail: 1,
56 //
57 // Set a base URL in order to shorten url command calls. If your `url` parameter starts
58 // with `/`, the base url gets prepended, not including the path portion of your baseUrl.
59 // If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
60 // gets prepended directly.
61 baseUrl: 'http://localhost:9001',
62 //
63 // Default timeout for all waitFor* commands.
64 waitforTimeout: 5000,
65 //
66 // Default timeout in milliseconds for request
67 // if browser driver or grid doesn't send response
68 connectionRetryTimeout: 120000,
69 //
70 // Default request retries count
71 connectionRetryCount: 3,
72
73 // Framework you want to run your specs with.
74 // The following are supported: Mocha, Jasmine, and Cucumber
75 // see also: https://webdriver.io/docs/frameworks
76 //
77 // Make sure you have the wdio adapter package for the specific framework installed
78 // before running any tests.
79 framework: 'mocha',
80 //
81 // The number of times to retry the entire specfile when it fails as a whole
82 // specFileRetries: 1,
83 //
84 // Delay in seconds between the spec file retry attempts
85 // specFileRetriesDelay: 0,
86 //
87 // Whether or not retried specfiles should be retried immediately or deferred to the end of the queue
88 // specFileRetriesDeferred: false,
89 //
90 // Test reporter for stdout.
91 // The only one supported by default is 'dot'
92 // see also: https://webdriver.io/docs/dot-reporter
93 reporters: [ 'spec' ],
94
95 //
96 // Options to be passed to Mocha.
97 // See the full list at http://mochajs.org/
98 mochaOpts: {
99 ui: 'bdd',
100 timeout: 60000
101 },
102
103 autoCompileOpts: {
104 autoCompile: true,
105
106 tsNodeOpts: {
107 project: require('path').join(__dirname, './tsconfig.json')
108 }
109 },
110
111 before: function () {
112 require('expect-webdriverio')
113 require('./src/commands/upload')
114 }
115} as Partial<WebdriverIO.Config>