diff options
Diffstat (limited to 'client/e2e')
-rw-r--r-- | client/e2e/local-protractor.conf.js | 56 | ||||
-rw-r--r-- | client/e2e/protractor.conf.js | 96 | ||||
-rw-r--r-- | client/e2e/proxy.config.json | 37 | ||||
-rw-r--r-- | client/e2e/src/commands/upload.ts | 12 | ||||
-rw-r--r-- | client/e2e/src/po/login.po.ts | 24 | ||||
-rw-r--r-- | client/e2e/src/po/my-account.ts | 102 | ||||
-rw-r--r-- | client/e2e/src/po/player.po.ts | 52 | ||||
-rw-r--r-- | client/e2e/src/po/video-update.po.ts | 12 | ||||
-rw-r--r-- | client/e2e/src/po/video-upload.po.ts | 34 | ||||
-rw-r--r-- | client/e2e/src/po/video-watch.po.ts | 132 | ||||
-rw-r--r-- | client/e2e/src/types/wdio.d.ts | 5 | ||||
-rw-r--r-- | client/e2e/src/utils.ts | 39 | ||||
-rw-r--r-- | client/e2e/src/videos.e2e-spec.ts | 140 | ||||
-rw-r--r-- | client/e2e/tsconfig.json | 13 | ||||
-rw-r--r-- | client/e2e/wdio.browserstack.conf.ts | 114 | ||||
-rw-r--r-- | client/e2e/wdio.local.conf.ts | 43 | ||||
-rw-r--r-- | client/e2e/wdio.main.conf.ts | 115 |
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 | |||
4 | const {SpecReporter} = require('jasmine-spec-reporter') | ||
5 | |||
6 | exports.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 | |||
4 | const {SpecReporter} = require('jasmine-spec-reporter') | ||
5 | |||
6 | exports.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 | |||
94 | exports.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 @@ | |||
1 | browser.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 @@ | |||
1 | import { browser, element, by } from 'protractor' | 1 | import { go } from '../utils' |
2 | 2 | ||
3 | export class LoginPage { | 3 | export 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 @@ | |||
1 | import { by, element, browser } from 'protractor' | 1 | import { go } from '../utils' |
2 | 2 | ||
3 | export class MyAccountPage { | 3 | export 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 @@ | |||
1 | import { browser, by, element } from 'protractor' | ||
2 | import { browserSleep, isIOS, isMobileDevice, isSafari } from '../utils' | 1 | import { browserSleep, isIOS, isMobileDevice, isSafari } from '../utils' |
3 | 2 | ||
4 | export class PlayerPage { | 3 | export 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 @@ | |||
1 | import { by, element } from 'protractor' | ||
2 | |||
3 | export class VideoUpdatePage { | 1 | export 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 @@ | |||
1 | import { browser, by, element } from 'protractor' | ||
2 | import { FileDetector } from 'selenium-webdriver/remote' | ||
3 | import { join } from 'path' | 1 | import { join } from 'path' |
4 | 2 | ||
5 | export class VideoUploadPage { | 3 | export 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 @@ | |||
1 | import { browser, by, element, ElementFinder, ExpectedConditions } from 'protractor' | 1 | import { browserSleep, go } from '../utils' |
2 | import { browserSleep, isMobileDevice } from '../utils' | ||
3 | 2 | ||
4 | export class VideoWatchPage { | 3 | export 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 @@ | |||
1 | declare 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 @@ | |||
1 | import { browser } from 'protractor' | ||
2 | |||
3 | async function browserSleep (amount: number) { | 1 | async 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) | 5 | function 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 | ||
15 | async function isMobileDevice () { | 11 | function 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 | ||
20 | async function isSafari () { | 16 | function 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 | ||
25 | async function isIOS () { | 20 | async 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 | ||
29 | export { | 31 | export { |
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 @@ | |||
1 | import { browser } from 'protractor' | ||
2 | import { LoginPage } from './po/login.po' | 1 | import { LoginPage } from './po/login.po' |
3 | import { MyAccountPage } from './po/my-account' | 2 | import { MyAccountPage } from './po/my-account' |
4 | import { PlayerPage } from './po/player.po' | 3 | import { PlayerPage } from './po/player.po' |
5 | import { VideoUpdatePage } from './po/video-update.po' | 4 | import { VideoUpdatePage } from './po/video-update.po' |
6 | import { VideoUploadPage } from './po/video-upload.po' | 5 | import { VideoUploadPage } from './po/video-upload.po' |
7 | import { VideoWatchPage } from './po/video-watch.po' | 6 | import { VideoWatchPage } from './po/video-watch.po' |
8 | import { isIOS, isMobileDevice, isSafari } from './utils' | 7 | import { browserSleep, go, isIOS, isMobileDevice, isSafari } from './utils' |
9 | 8 | ||
10 | async function skipIfUploadNotSupported () { | 9 | function 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 @@ | |||
1 | import { config as mainConfig } from './wdio.main.conf' | ||
2 | |||
3 | const user = process.env.BROWSERSTACK_USER | ||
4 | const key = process.env.BROWSERSTACK_KEY | ||
5 | |||
6 | if (!user) throw new Error('Miss browser stack user') | ||
7 | if (!key) throw new Error('Miss browser stack key') | ||
8 | |||
9 | function 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 | |||
19 | function buildBStackDesktopOptions (sessionName: string, resolution?: string) { | ||
20 | return { | ||
21 | 'bstack:options': { | ||
22 | ...buildMainOptions(sessionName), | ||
23 | |||
24 | resolution | ||
25 | } | ||
26 | } | ||
27 | } | ||
28 | |||
29 | function 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 | |||
41 | module.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 @@ | |||
1 | import { config as mainConfig } from './wdio.main.conf' | ||
2 | |||
3 | const prefs = { | ||
4 | 'intl.accept_languages': 'en' | ||
5 | } | ||
6 | |||
7 | module.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 @@ | |||
1 | export 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> | ||