Browse Source

Fix NSFW filter and add tests

Chocobozzz 2 years ago
parent
commit
6d210220be

+ 1 - 0
client/angular.json

@@ -281,6 +281,7 @@
           "builder": "@angular-eslint/builder:lint",
           "options": {
             "lintFilePatterns": [
+              "e2e/**/*.ts",
               "src/**/*.ts",
               "src/**/*.html"
             ]

+ 29 - 0
client/e2e/src/po/admin-config.po.ts

@@ -0,0 +1,29 @@
+import { browserSleep, go } from '../utils'
+
+export class AdminConfigPage {
+
+  async navigateTo (tab: 'instance-homepage' | 'basic-configuration' | 'instance-information') {
+    const waitTitles = {
+      'instance-homepage': 'INSTANCE HOMEPAGE',
+      'basic-configuration': 'APPEARANCE',
+      'instance-information': 'INSTANCE'
+    }
+
+    await go('/admin/config/edit-custom#' + tab)
+
+    await $('.inner-form-title=' + waitTitles[tab]).waitForDisplayed()
+  }
+
+  updateNSFWSetting (newValue: 'do_not_list' | 'blur' | 'display') {
+    return $('#instanceDefaultNSFWPolicy').selectByAttribute('value', newValue)
+  }
+
+  updateHomepage (newValue: string) {
+    return $('#instanceCustomHomepageContent').setValue(newValue)
+  }
+
+  async save () {
+    await $('input[type=submit]').click()
+    await browserSleep(200)
+  }
+}

+ 19 - 1
client/e2e/src/po/login.po.ts

@@ -1,6 +1,7 @@
 import { go } from '../utils'
 
 export class LoginPage {
+
   async loginAsRootUser () {
     await go('/login')
 
@@ -8,7 +9,7 @@ export class LoginPage {
     await browser.execute(`window.localStorage.setItem('no_welcome_modal', 'true')`)
 
     await $('input#username').setValue('root')
-    await $('input#password').setValue('test1')
+    await $('input#password').setValue('test' + this.getSuffix())
 
     await browser.pause(1000)
 
@@ -19,7 +20,24 @@ export class LoginPage {
     await expect(this.getLoggedInInfoElem()).toHaveText('root')
   }
 
+  async logout () {
+    await $('.logged-in-more').click()
+
+    const logout = () => $('.dropdown-item*=Log out')
+
+    await logout().waitForDisplayed()
+    await logout().click()
+
+    await $('.login-buttons-block').waitForDisplayed()
+  }
+
   private getLoggedInInfoElem () {
     return $('.logged-in-display-name')
   }
+
+  private getSuffix () {
+    return browser.config.baseUrl
+      ? browser.config.baseUrl.slice(-1)
+      : '1'
+  }
 }

+ 18 - 0
client/e2e/src/po/my-account.ts

@@ -14,6 +14,24 @@ export class MyAccountPage {
     return $('a[href="/my-library/history/videos"]').click()
   }
 
+  // Settings
+
+  navigateToMySettings () {
+    return $('a[href="/my-account"]').click()
+  }
+
+  async updateNSFW (newValue: 'do_not_list' | 'blur' | 'display') {
+    const nsfw = $('#nsfwPolicy')
+
+    await nsfw.waitForDisplayed()
+    await nsfw.scrollIntoView(false) // Avoid issues with fixed header on firefox
+    await nsfw.selectByAttribute('value', newValue)
+
+    const submit = $('my-user-video-settings input[type=submit]')
+    await submit.scrollIntoView(false)
+    await submit.click()
+  }
+
   // My account Videos
 
   async removeVideo (name: string) {

+ 4 - 1
client/e2e/src/po/player.po.ts

@@ -15,6 +15,9 @@ export class PlayerPage {
 
   waitUntilPlaylistInfo (text: string, maxTime: number) {
     return browser.waitUntil(async () => {
+      // Without this we have issues on iphone
+      await $('.video-js').click()
+
       return (await $('.video-js .vjs-playlist-info').getText()).includes(text)
     }, { timeout: maxTime })
   }
@@ -42,7 +45,7 @@ export class PlayerPage {
     await browserSleep(2000)
 
     await browser.waitUntil(async () => {
-      return (await this.getWatchVideoPlayerCurrentTime()) >= 2
+      return (await this.getWatchVideoPlayerCurrentTime()) >= waitUntilSec
     })
 
     await videojsElem().click()

+ 128 - 0
client/e2e/src/po/video-list.po.ts

@@ -0,0 +1,128 @@
+import { browserSleep, go } from '../utils'
+
+export class VideoListPage {
+
+  constructor (private isMobileDevice: boolean, private isSafari: boolean) {
+
+  }
+
+  async goOnVideosList () {
+    let url: string
+
+    // We did not upload a file on a mobile device
+    if (this.isMobileDevice === true || this.isSafari === true) {
+      url = 'https://peertube2.cpy.re/videos/local'
+    } else {
+      url = '/videos/recently-added'
+    }
+
+    await go(url)
+
+    // Waiting the following element does not work on Safari...
+    if (this.isSafari) return browserSleep(3000)
+
+    await this.waitForList()
+  }
+
+  async goOnLocal () {
+    await $('.menu-link[href="/videos/local"]').click()
+    await this.waitForTitle('Local videos')
+  }
+
+  async goOnRecentlyAdded () {
+    await $('.menu-link[href="/videos/recently-added"]').click()
+    await this.waitForTitle('Recently added')
+  }
+
+  async goOnTrending () {
+    await $('.menu-link[href="/videos/trending"]').click()
+    await this.waitForTitle('Trending')
+  }
+
+  async goOnHomepage () {
+    await go('/home')
+    await this.waitForList()
+  }
+
+  async goOnRootChannel () {
+    await go('/c/root_channel/videos')
+    await this.waitForList()
+  }
+
+  async goOnRootAccount () {
+    await go('/a/root/videos')
+    await this.waitForList()
+  }
+
+  async goOnRootAccountChannels () {
+    await go('/a/root/video-channels')
+    await this.waitForList()
+  }
+
+  getNSFWFilter () {
+    return $$('.active-filter').filter(async a => {
+      return (await a.getText()).includes('Sensitive')
+    }).then(f => f[0])
+  }
+
+  async getVideosListName () {
+    const elems = await $$('.videos .video-miniature .video-miniature-name')
+    const texts = await Promise.all(elems.map(e => e.getText()))
+
+    return texts.map(t => t.trim())
+  }
+
+  videoExists (name: string) {
+    return $('.video-miniature-name=' + name).isDisplayed()
+  }
+
+  async videoIsBlurred (name: string) {
+    const filter = await $('.video-miniature-name=' + name).getCSSProperty('filter')
+
+    return filter.value !== 'none'
+  }
+
+  async clickOnVideo (videoName: string) {
+    const video = async () => {
+      const videos = await $$('.videos .video-miniature .video-miniature-name').filter(async e => {
+        const t = await e.getText()
+
+        return t === videoName
+      })
+
+      return videos[0]
+    }
+
+    await browser.waitUntil(async () => {
+      const elem = await video()
+
+      return elem?.isClickable()
+    });
+
+    (await video()).click()
+
+    await browser.waitUntil(async () => (await browser.getUrl()).includes('/w/'))
+  }
+
+  async clickOnFirstVideo () {
+    const video = () => $('.videos .video-miniature .video-thumbnail')
+    const videoName = () => $('.videos .video-miniature .video-miniature-name')
+
+    await video().waitForClickable()
+
+    const textToReturn = await videoName().getText()
+    await video().click()
+
+    await browser.waitUntil(async () => (await browser.getUrl()).includes('/w/'))
+
+    return textToReturn
+  }
+
+  private waitForList () {
+    return $('.videos .video-miniature .video-miniature-name').waitForDisplayed()
+  }
+
+  private waitForTitle (title: string) {
+    return $('h1=' + title).waitForDisplayed()
+  }
+}

+ 11 - 0
client/e2e/src/po/video-search.po.ts

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

+ 5 - 0
client/e2e/src/po/video-upload.po.ts

@@ -1,4 +1,5 @@
 import { join } from 'path'
+import { clickOnCheckbox } from '../utils'
 
 export class VideoUploadPage {
   async navigateTo () {
@@ -30,6 +31,10 @@ export class VideoUploadPage {
     })
   }
 
+  setAsNSFW () {
+    return clickOnCheckbox('nsfw')
+  }
+
   async validSecondUploadStep (videoName: string) {
     const nameInput = $('input#name')
     await nameInput.clearValue()

+ 5 - 62
client/e2e/src/po/video-watch.po.ts

@@ -1,37 +1,16 @@
-import { FIXTURE_URLS } from '../urls'
-import { browserSleep, go } from '../utils'
+import { browserSleep, FIXTURE_URLS, go } from '../utils'
 
 export class VideoWatchPage {
-  async goOnVideosList (isMobileDevice: boolean, isSafari: boolean) {
-    let url: string
-
-    // We did not upload a file on a mobile device
-    if (isMobileDevice === true || isSafari === true) {
-      url = 'https://peertube2.cpy.re/videos/local'
-    } else {
-      url = '/videos/recently-added'
-    }
-
-    await go(url)
-
-    // Waiting the following element does not work on Safari...
-    if (isSafari) return browserSleep(3000)
 
-    await $('.videos .video-miniature .video-miniature-name').waitForDisplayed()
-  }
-
-  async getVideosListName () {
-    const elems = await $$('.videos .video-miniature .video-miniature-name')
-    const texts = await Promise.all(elems.map(e => e.getText()))
+  constructor (private isMobileDevice: boolean, private isSafari: boolean) {
 
-    return texts.map(t => t.trim())
   }
 
-  waitWatchVideoName (videoName: string, isMobileDevice: boolean, isSafari: boolean) {
-    if (isSafari) return browserSleep(5000)
+  waitWatchVideoName (videoName: string) {
+    if (this.isSafari) return browserSleep(5000)
 
     // On mobile we display the first node, on desktop the second
-    const index = isMobileDevice ? 0 : 1
+    const index = this.isMobileDevice ? 0 : 1
 
     return browser.waitUntil(async () => {
       return (await $$('.video-info .video-info-name')[index].getText()).includes(videoName)
@@ -58,42 +37,6 @@ export class VideoWatchPage {
     return go(FIXTURE_URLS.HLS_PLAYLIST_EMBED)
   }
 
-  async clickOnVideo (videoName: string) {
-    const video = async () => {
-      const videos = await $$('.videos .video-miniature .video-miniature-name').filter(async e => {
-        const t = await e.getText()
-
-        return t === videoName
-      })
-
-      return videos[0]
-    }
-
-    await browser.waitUntil(async () => {
-      const elem = await video()
-
-      return elem?.isClickable()
-    });
-
-    (await video()).click()
-
-    await browser.waitUntil(async () => (await browser.getUrl()).includes('/w/'))
-  }
-
-  async clickOnFirstVideo () {
-    const video = () => $('.videos .video-miniature .video-thumbnail')
-    const videoName = () => $('.videos .video-miniature .video-miniature-name')
-
-    await video().waitForClickable()
-
-    const textToReturn = await videoName().getText()
-    await video().click()
-
-    await browser.waitUntil(async () => (await browser.getUrl()).includes('/w/'))
-
-    return textToReturn
-  }
-
   async clickOnUpdate () {
     const dropdown = $('my-video-actions-dropdown .action-button')
     await dropdown.click()

+ 17 - 20
client/e2e/src/videos.e2e-spec.ts → client/e2e/src/suites-all/videos.e2e-spec.ts

@@ -1,11 +1,11 @@
-import { LoginPage } from './po/login.po'
-import { MyAccountPage } from './po/my-account'
-import { PlayerPage } from './po/player.po'
-import { VideoUpdatePage } from './po/video-update.po'
-import { VideoUploadPage } from './po/video-upload.po'
-import { VideoWatchPage } from './po/video-watch.po'
-import { FIXTURE_URLS } from './urls'
-import { browserSleep, go, isIOS, isMobileDevice, isSafari } from './utils'
+import { LoginPage } from '../po/login.po'
+import { MyAccountPage } from '../po/my-account'
+import { PlayerPage } from '../po/player.po'
+import { VideoListPage } from '../po/video-list.po'
+import { VideoUpdatePage } from '../po/video-update.po'
+import { VideoUploadPage } from '../po/video-upload.po'
+import { VideoWatchPage } from '../po/video-watch.po'
+import { FIXTURE_URLS, go, isIOS, isMobileDevice, isSafari, waitServerUp } from '../utils'
 
 function isUploadUnsupported () {
   if (isMobileDevice() || isSafari()) {
@@ -16,8 +16,9 @@ function isUploadUnsupported () {
   return false
 }
 
-describe('Videos workflow', () => {
+describe('Videos all workflow', () => {
   let videoWatchPage: VideoWatchPage
+  let videoListPage: VideoListPage
   let videoUploadPage: VideoUploadPage
   let videoUpdatePage: VideoUpdatePage
   let myAccountPage: MyAccountPage
@@ -40,21 +41,17 @@ describe('Videos workflow', () => {
 
     if (isUploadUnsupported()) return
 
-    await browser.waitUntil(async () => {
-      await go('/')
-      await browserSleep(500)
-
-      return $('<my-app>').isDisplayed()
-    }, { timeout: 20 * 1000 })
+    await waitServerUp()
   })
 
   beforeEach(async () => {
-    videoWatchPage = new VideoWatchPage()
+    videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
     videoUploadPage = new VideoUploadPage()
     videoUpdatePage = new VideoUpdatePage()
     myAccountPage = new MyAccountPage()
     loginPage = new LoginPage()
     playerPage = new PlayerPage()
+    videoListPage = new VideoListPage(isMobileDevice(), isSafari())
 
     if (!isMobileDevice()) {
       await browser.maximizeWindow()
@@ -80,11 +77,11 @@ describe('Videos workflow', () => {
   })
 
   it('Should list videos', async () => {
-    await videoWatchPage.goOnVideosList(isMobileDevice(), isSafari())
+    await videoListPage.goOnVideosList()
 
     if (isUploadUnsupported()) return
 
-    const videoNames = await videoWatchPage.getVideosListName()
+    const videoNames = await videoListPage.getVideosListName()
     expect(videoNames).toContain(videoName)
   })
 
@@ -95,10 +92,10 @@ describe('Videos workflow', () => {
       await go(FIXTURE_URLS.WEBTORRENT_VIDEO)
       videoNameToExcept = 'E2E tests'
     } else {
-      await videoWatchPage.clickOnVideo(videoName)
+      await videoListPage.clickOnVideo(videoName)
     }
 
-    return videoWatchPage.waitWatchVideoName(videoNameToExcept, isMobileDevice(), isSafari())
+    return videoWatchPage.waitWatchVideoName(videoNameToExcept)
   })
 
   it('Should play the video', async () => {

+ 195 - 0
client/e2e/src/suites-local/videos-list.e2e-spec.ts

@@ -0,0 +1,195 @@
+import { AdminConfigPage } from '../po/admin-config.po'
+import { LoginPage } from '../po/login.po'
+import { MyAccountPage } from '../po/my-account'
+import { VideoListPage } from '../po/video-list.po'
+import { VideoSearchPage } from '../po/video-search.po'
+import { VideoUploadPage } from '../po/video-upload.po'
+import { NSFWPolicy } from '../types/common'
+import { isMobileDevice, isSafari, waitServerUp } from '../utils'
+
+describe('Videos list', () => {
+  let videoListPage: VideoListPage
+  let videoUploadPage: VideoUploadPage
+  let adminConfigPage: AdminConfigPage
+  let loginPage: LoginPage
+  let myAccountPage: MyAccountPage
+  let videoSearchPage: VideoSearchPage
+
+  const seed = Math.random()
+  const nsfwVideo = seed + ' - nsfw'
+  const normalVideo = seed + ' - normal'
+
+  async function checkNormalVideo () {
+    expect(await videoListPage.videoExists(normalVideo)).toBeTruthy()
+    expect(await videoListPage.videoIsBlurred(normalVideo)).toBeFalsy()
+  }
+
+  async function checkNSFWVideo (policy: NSFWPolicy, filterText?: string) {
+    if (policy === 'do_not_list') {
+      if (filterText) expect(filterText).toContain('hidden')
+
+      expect(await videoListPage.videoExists(nsfwVideo)).toBeFalsy()
+      return
+    }
+
+    if (policy === 'blur') {
+      if (filterText) expect(filterText).toContain('blurred')
+
+      expect(await videoListPage.videoExists(nsfwVideo)).toBeTruthy()
+      expect(await videoListPage.videoIsBlurred(nsfwVideo)).toBeTruthy()
+      return
+    }
+
+    // display
+    if (filterText) expect(filterText).toContain('displayed')
+
+    expect(await videoListPage.videoExists(nsfwVideo)).toBeTruthy()
+    expect(await videoListPage.videoIsBlurred(nsfwVideo)).toBeFalsy()
+  }
+
+  async function checkCommonVideoListPages (policy: NSFWPolicy) {
+    const promisesWithFilters = [
+      videoListPage.goOnRootAccount,
+      videoListPage.goOnLocal,
+      videoListPage.goOnRecentlyAdded,
+      videoListPage.goOnTrending,
+      videoListPage.goOnRootChannel
+    ]
+
+    for (const p of promisesWithFilters) {
+      await p.call(videoListPage)
+
+      const filter = await videoListPage.getNSFWFilter()
+      const filterText = await filter.getText()
+
+      await checkNormalVideo()
+      await checkNSFWVideo(policy, filterText)
+    }
+
+    const promisesWithoutFilters = [
+      videoListPage.goOnRootAccountChannels,
+      videoListPage.goOnHomepage
+    ]
+    for (const p of promisesWithoutFilters) {
+      await p.call(videoListPage)
+
+      await checkNormalVideo()
+      await checkNSFWVideo(policy)
+    }
+  }
+
+  async function checkSearchPage (policy: NSFWPolicy) {
+    await videoSearchPage.search(normalVideo)
+    await checkNormalVideo()
+
+    await videoSearchPage.search(nsfwVideo)
+    await checkNSFWVideo(policy)
+  }
+
+  async function updateAdminNSFW (nsfw: NSFWPolicy) {
+    await adminConfigPage.navigateTo('instance-information')
+    await adminConfigPage.updateNSFWSetting(nsfw)
+    await adminConfigPage.save()
+  }
+
+  async function updateUserNSFW (nsfw: NSFWPolicy) {
+    await myAccountPage.navigateToMySettings()
+    await myAccountPage.updateNSFW(nsfw)
+  }
+
+  before(async () => {
+    await waitServerUp()
+  })
+
+  beforeEach(async () => {
+    videoListPage = new VideoListPage(isMobileDevice(), isSafari())
+    adminConfigPage = new AdminConfigPage()
+    loginPage = new LoginPage()
+    videoUploadPage = new VideoUploadPage()
+    myAccountPage = new MyAccountPage()
+    videoSearchPage = new VideoSearchPage()
+
+    await browser.maximizeWindow()
+  })
+
+  it('Should login and disable NSFW', async () => {
+    await loginPage.loginAsRootUser()
+    await updateUserNSFW('display')
+  })
+
+  it('Should set the homepage', async () => {
+    await adminConfigPage.navigateTo('instance-homepage')
+    await adminConfigPage.updateHomepage('<peertube-videos-list data-sort="-publishedAt"></peertube-videos-list>')
+    await adminConfigPage.save()
+  })
+
+  it('Should upload 2 videos (NSFW and classic videos)', async () => {
+    await videoUploadPage.navigateTo()
+    await videoUploadPage.uploadVideo()
+    await videoUploadPage.setAsNSFW()
+    await videoUploadPage.validSecondUploadStep(nsfwVideo)
+
+    await videoUploadPage.navigateTo()
+    await videoUploadPage.uploadVideo()
+    await videoUploadPage.validSecondUploadStep(normalVideo)
+  })
+
+  it('Should logout', async function () {
+    await loginPage.logout()
+  })
+
+  describe('Anonymous users', function () {
+
+    it('Should correctly handle do not list', async () => {
+      await loginPage.loginAsRootUser()
+      await updateAdminNSFW('do_not_list')
+
+      await loginPage.logout()
+      await checkCommonVideoListPages('do_not_list')
+      await checkSearchPage('do_not_list')
+    })
+
+    it('Should correctly handle blur', async () => {
+      await loginPage.loginAsRootUser()
+      await updateAdminNSFW('blur')
+
+      await loginPage.logout()
+      await checkCommonVideoListPages('blur')
+      await checkSearchPage('blur')
+    })
+
+    it('Should correctly handle display', async () => {
+      await loginPage.loginAsRootUser()
+      await updateAdminNSFW('display')
+
+      await loginPage.logout()
+      await checkCommonVideoListPages('display')
+      await checkSearchPage('display')
+    })
+  })
+
+  describe('Logged in users', function () {
+
+    before(async () => {
+      await loginPage.loginAsRootUser()
+    })
+
+    it('Should correctly handle do not list', async () => {
+      await updateUserNSFW('do_not_list')
+      await checkCommonVideoListPages('do_not_list')
+      await checkSearchPage('do_not_list')
+    })
+
+    it('Should correctly handle blur', async () => {
+      await updateUserNSFW('blur')
+      await checkCommonVideoListPages('blur')
+      await checkSearchPage('blur')
+    })
+
+    it('Should correctly handle display', async () => {
+      await updateUserNSFW('display')
+      await checkCommonVideoListPages('display')
+      await checkSearchPage('display')
+    })
+  })
+})

+ 1 - 0
client/e2e/src/types/common.ts

@@ -0,0 +1 @@
+export type NSFWPolicy = 'do_not_list' | 'blur' | 'display'

+ 10 - 0
client/e2e/src/utils.ts → client/e2e/src/utils/common.ts

@@ -28,10 +28,20 @@ async function go (url: string) {
   })
 }
 
+async function waitServerUp () {
+  await browser.waitUntil(async () => {
+    await go('/')
+    await browserSleep(500)
+
+    return $('<my-app>').isDisplayed()
+  }, { timeout: 20 * 1000 })
+}
+
 export {
   isMobileDevice,
   isSafari,
   isIOS,
+  waitServerUp,
   go,
   browserSleep
 }

+ 7 - 0
client/e2e/src/utils/elements.ts

@@ -0,0 +1,7 @@
+function clickOnCheckbox (name: string) {
+  return $(`my-peertube-checkbox[inputname=${name}] label`).click()
+}
+
+export {
+  clickOnCheckbox
+}

+ 3 - 0
client/e2e/src/utils/index.ts

@@ -0,0 +1,3 @@
+export * from './common'
+export * from './elements'
+export * from './urls'

+ 0 - 0
client/e2e/src/urls.ts → client/e2e/src/utils/urls.ts


+ 2 - 0
client/e2e/tsconfig.json

@@ -2,6 +2,8 @@
   "extends": "../tsconfig.json",
   "compilerOptions": {
     "outDir": "../out-tsc/app",
+    "noImplicitAny": false,
+    "esModuleInterop": true,
     "module": "commonjs",
     "target": "es5",
     "types": [

+ 13 - 8
client/e2e/wdio.browserstack.conf.ts

@@ -26,14 +26,16 @@ function buildBStackDesktopOptions (sessionName: string, resolution?: string) {
   }
 }
 
-function buildBStackMobileOptions (sessionName: string, deviceName: string, osVersion: string) {
+function buildBStackMobileOptions (sessionName: string, deviceName: string, osVersion: string, appiumVersion?: string) {
   return {
     'bstack:options': {
       ...buildMainOptions(sessionName),
 
       realMobile: true,
       osVersion,
-      deviceName
+      deviceName,
+
+      appiumVersion
     }
   }
 }
@@ -84,7 +86,7 @@ module.exports = {
       {
         browserName: 'Safari',
 
-        ...buildBStackMobileOptions('Safari iPhone', 'iPhone 8 Plus', '11')
+        ...buildBStackMobileOptions('Safari iPhone', 'iPhone SE', '11')
       },
       {
         browserName: 'Safari',
@@ -97,17 +99,20 @@ module.exports = {
     connectionRetryTimeout: 240000,
     waitforTimeout: 20000,
 
+    specs: [
+      // We don't want to test "local" tests
+      './src/suites-all/*.e2e-spec.ts'
+    ],
+
     services: [
       [
         'browserstack', { browserstackLocal: true }
       ]
     ],
 
-    after: function (result) {
-      if (result === 0) {
-        browser.executeScript('browserstack_executor: {"action": "setSessionStatus", "arguments": {"status":"passed","reason": ""}}', [])
-      } else {
-        browser.executeScript('browserstack_executor: {"action": "setSessionStatus", "arguments": {"status":"failed","reason": ""}}', [])
+    onWorkerStart: function (_cid, capabilities) {
+      if (capabilities['bstack:options'].realMobile === true) {
+        capabilities['bstack:options'].local = false
       }
     }
   } as WebdriverIO.Config

+ 28 - 0
client/e2e/wdio.local-test.conf.ts

@@ -0,0 +1,28 @@
+import { config as mainConfig } from './wdio.main.conf'
+
+const prefs = {
+  'intl.accept_languages': 'en'
+}
+
+module.exports = {
+  config: {
+    ...mainConfig,
+
+    runner: 'local',
+
+    maxInstances: 1,
+    specFileRetries: 0,
+
+    capabilities: [
+      {
+        browserName: 'chrome',
+        acceptInsecureCerts: true,
+        'goog:chromeOptions': {
+          prefs
+        }
+      }
+    ],
+
+    services: [ 'chromedriver' ]
+  } as WebdriverIO.Config
+}

+ 10 - 12
client/e2e/wdio.local.conf.ts

@@ -10,12 +10,11 @@ module.exports = {
 
     runner: 'local',
 
-    maxInstances: 1,
+    maxInstances: 2,
 
     capabilities: [
       {
         browserName: 'chrome',
-        acceptInsecureCerts: true,
         'goog:chromeOptions': {
           prefs
         }
@@ -23,21 +22,20 @@ module.exports = {
       {
         browserName: 'firefox',
         'moz:firefoxOptions': {
-          // args: [ '-headless' ],
           binary: '/usr/bin/firefox-developer-edition',
           prefs
         }
-      },
-      {
-        browserName: 'firefox',
-        'moz:firefoxOptions': {
-          // args: [ '-headless' ],
-          binary: '/usr/bin/firefox-esr',
-          prefs
-        }
       }
     ],
 
-    services: [ 'chromedriver', 'geckodriver' ]
+    services: [ 'chromedriver', 'geckodriver' ],
+
+    beforeSession: function (config, capabilities) {
+      if (capabilities['browserName'] === 'chrome') {
+        config.baseUrl = 'http://localhost:9001'
+      } else {
+        config.baseUrl = 'http://localhost:9002'
+      }
+    }
   } as WebdriverIO.Config
 }

+ 11 - 2
client/e2e/wdio.main.conf.ts

@@ -21,7 +21,8 @@ export const config = {
   // will be called from there.
   //
   specs: [
-    './src/**/*.e2e-spec.ts'
+    './src/suites-all/*.e2e-spec.ts',
+    './src/suites-local/*.e2e-spec.ts'
   ],
   // Patterns to exclude.
   exclude: [
@@ -79,7 +80,7 @@ export const config = {
   framework: 'mocha',
   //
   // The number of times to retry the entire specfile when it fails as a whole
-  specFileRetries: 2,
+  specFileRetries: 1,
   //
   // Delay in seconds between the spec file retry attempts
   // specFileRetriesDelay: 0,
@@ -105,6 +106,14 @@ export const config = {
 
     tsNodeOpts: {
       project: require('path').join(__dirname, './tsconfig.json')
+    },
+
+    tsConfigPathsOpts: {
+      baseUrl: './',
+      paths: {
+        '@server/*': [ '../../server/*' ],
+        '@shared/*': [ '../../shared/*' ]
+      }
     }
   },
 

+ 5 - 1
client/src/app/core/users/user.service.ts

@@ -17,6 +17,7 @@ import {
   UserUpdateMe,
   UserVideoQuota
 } from '@shared/models'
+import { ServerService } from '../'
 import { environment } from '../../../environments/environment'
 import { RestExtractor, RestPagination, RestService } from '../rest'
 import { LocalStorageService, SessionStorageService } from '../wrappers/storage.service'
@@ -32,6 +33,7 @@ export class UserService {
 
   constructor (
     private authHttp: HttpClient,
+    private server: ServerService,
     private authService: AuthService,
     private restExtractor: RestExtractor,
     private restService: RestService,
@@ -298,9 +300,11 @@ export class UserService {
       console.error('Cannot parse desired video languages from localStorage.', err)
     }
 
+    const defaultNSFWPolicy = this.server.getHTMLConfig().instance.defaultNSFWPolicy
+
     return new User({
       // local storage keys
-      nsfwPolicy: this.localStorageService.getItem(UserLocalStorageKeys.NSFW_POLICY),
+      nsfwPolicy: this.localStorageService.getItem(UserLocalStorageKeys.NSFW_POLICY) || defaultNSFWPolicy,
       webTorrentEnabled: this.localStorageService.getItem(UserLocalStorageKeys.WEBTORRENT_ENABLED) !== 'false',
       theme: this.localStorageService.getItem(UserLocalStorageKeys.THEME) || 'instance-default',
       videoLanguages,

+ 0 - 6
client/src/app/shared/shared-video-miniature/video-filters-header.component.ts

@@ -5,7 +5,6 @@ import { FormBuilder, FormGroup } from '@angular/forms'
 import { AuthService } from '@app/core'
 import { ServerService } from '@app/core/server/server.service'
 import { UserRight } from '@shared/models'
-import { NSFWPolicyType } from '@shared/models/videos'
 import { PeertubeModalService } from '../shared-main'
 import { VideoFilters } from './video-filters.model'
 
@@ -18,12 +17,7 @@ const logger = debug('peertube:videos:VideoFiltersHeaderComponent')
 })
 export class VideoFiltersHeaderComponent implements OnInit, OnDestroy {
   @Input() filters: VideoFilters
-
   @Input() displayModerationBlock = false
-
-  @Input() defaultSort = '-publishedAt'
-  @Input() nsfwPolicy: NSFWPolicyType
-
   @Input() hideScope = false
 
   @Output() filtersChanged = new EventEmitter()

+ 2 - 0
client/src/app/shared/shared-video-miniature/video-filters.model.ts

@@ -74,6 +74,8 @@ export class VideoFilters {
   }
 
   setNSFWPolicy (nsfwPolicy: NSFWPolicyType) {
+    console.log(nsfwPolicy)
+
     this.updateDefaultNSFW(nsfwPolicy)
   }
 

+ 1 - 1
client/src/app/shared/shared-video-miniature/videos-list.component.html

@@ -34,7 +34,7 @@
 
   <my-video-filters-header
     *ngIf="displayFilters" [displayModerationBlock]="displayModerationBlock" [hideScope]="hideScopeFilter"
-    [defaultSort]="defaultSort" [filters]="filters"
+    [filters]="filters"
     (filtersChanged)="onFiltersChanged(true)"
   ></my-video-filters-header>
 

+ 1 - 1
scripts/e2e/browserstack.sh

@@ -6,4 +6,4 @@ npm run clean:server:test
 
 npm run concurrently -- -k -s first \
     "cd client/e2e && ../node_modules/.bin/wdio run ./wdio.browserstack.conf.ts" \
-    "NODE_ENV=test NODE_APP_INSTANCE=1 NODE_CONFIG='{ \"log\": { \"level\": \"warn\" }, \"signup\": { \"enabled\": false } }' node dist/server"
+    "NODE_ENV=test NODE_APP_INSTANCE=1 NODE_CONFIG='{ \"rates_limit\": { \"api\": { \"max\": 5000 }, \"login\": { \"max\": 5000 } }, \"log\": { \"level\": \"warn\" }, \"signup\": { \"enabled\": false } }' node dist/server"

+ 9 - 1
scripts/e2e/local.sh

@@ -4,6 +4,14 @@ set -eu
 
 npm run clean:server:test
 
+config="{"
+config+="  \"rates_limit\": { \"api\": { \"max\": 5000 }, \"login\": { \"max\": 5000 } }"
+config+=", \"log\": { \"level\": \"warn\" }"
+config+=", \"signup\": { \"enabled\": false }"
+config+=", \"transcoding\": { \"enabled\": false }"
+config+="}"
+
 npm run concurrently -- -k -s first \
     "cd client/e2e && ../node_modules/.bin/wdio run ./wdio.local.conf.ts" \
-    "NODE_ENV=test NODE_APP_INSTANCE=1 NODE_CONFIG='{ \"log\": { \"level\": \"warn\" }, \"signup\": { \"enabled\": false } }' node dist/server"
+    "NODE_ENV=test NODE_CONFIG='$config' NODE_APP_INSTANCE=1  node dist/server" \
+    "NODE_ENV=test NODE_CONFIG='$config' NODE_APP_INSTANCE=2  node dist/server"

+ 15 - 0
support/doc/development/tests.md

@@ -70,3 +70,18 @@ To run tests on browser stack:
 ```
 $ BROWSERSTACK_USER=your_user BROWSERSTACK_KEY=your_key npm run e2e:browserstack
 ```
+
+### Add E2E tests
+
+To add E2E tests and quickly run tests using a local Chrome, first create a test instance:
+
+```
+$ npm run clean:server:test && NODE_APP_INSTANCE=1 NODE_ENV=test npm start
+```
+
+Then, just run your suite using:
+
+```
+$ cd client/e2e
+$ ../node_modules/.bin/wdio wdio.local-test.conf.ts # you can also add --mochaOpts.grep to only run tests you want
+```