Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 70 additions & 2 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ on:
permissions:
contents: read

concurrency:
group: playwright-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
playwright:
timeout-minutes: 60
Expand Down Expand Up @@ -73,6 +77,7 @@ jobs:
run: npx playwright test ${{ matrix.project-args }} --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env:
COLLECTIVES_SEMANTIC_E2E: ${{ matrix.semantic-e2e }}
COLLECTIVES_COMPARISON_INITIAL_VIEW: changes
PLAYWRIGHT_NC_SERVER_BRANCH: ${{ matrix.server-branch }}
PLAYWRIGHT_TEXT_REPOSITORY: ${{ matrix.text-repository }}
PLAYWRIGHT_TEXT_REF: ${{ matrix.text-ref }}
Expand All @@ -87,11 +92,74 @@ jobs:
blob-report/
retention-days: 7

comparison-nc35:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- name: Checkout app
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Check composer.json
id: check_composer
uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v3.1.0
with:
files: 'composer.json'

- name: Install composer dependencies
if: steps.check_composer.outputs.files_exists == 'true'
run: composer install --no-dev

- name: Read package.json
uses: nextcloud-libraries/parse-package-engines-action@122ae05d4257008180a514e1ddeb0c1b9d094bdd # v0.1.0
id: versions

- name: Set up node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ steps.versions.outputs.node-version }}

- name: Set up npm
run: npm i -g 'npm@${{ steps.versions.outputs.package-manager-version }}'

- name: Install node dependencies & build app
env:
CYPRESS_INSTALL_BINARY: 0
run: |
npm ci
npm run build --if-present

- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium

- name: Run Playwright tests
run: ./node_modules/.bin/playwright test playwright/e2e/version-comparison.spec.ts --project=comparison-chromium --workers=1
env:
COLLECTIVES_SEMANTIC_E2E: '1'
COLLECTIVES_COMPARISON_INITIAL_VIEW: changes
NEXTCLOUD_E2E_LOG_FILE: test-results/nextcloud.log
PLAYWRIGHT_NC_SERVER_BRANCH: stable35
PLAYWRIGHT_TEXT_REPOSITORY: nextcloud/text
PLAYWRIGHT_TEXT_REF: 'b07c97d488f7a2c44edea8b2ad3b88a85f6612bf'

- name: Upload results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ !cancelled() }}
with:
name: playwright-report_stable35_comparison
path: |
test-results/
blob-report/
retention-days: 7

summary:
runs-on: ubuntu-latest-low
needs: playwright
needs: [playwright, comparison-nc35]
if: always()
name: playwright-summary
steps:
- name: Summary status
run: if ${{ needs.playwright.result != 'success' && needs.playwright.result != 'skipped' }}; then exit 1; fi
run: |
if ${{ needs.playwright.result != 'success' && needs.playwright.result != 'skipped' }}; then exit 1; fi
if ${{ needs.comparison-nc35.result != 'success' }}; then exit 1; fi
39 changes: 22 additions & 17 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ const CI_CONFIG = {
const baseURL = process.env.baseURL || 'http://localhost:8089/index.php/'
const externalServer = Boolean(process.env.baseURL)
const SEMANTIC_E2E = process.env.COLLECTIVES_SEMANTIC_E2E === '1'
const comparisonInitialView = process.env.COLLECTIVES_COMPARISON_INITIAL_VIEW ?? (process.env.CI ? undefined : 'changes')
if (SEMANTIC_E2E && comparisonInitialView !== 'changes' && comparisonInitialView !== 'documents') {
throw new Error('Set COLLECTIVES_COMPARISON_INITIAL_VIEW to changes or documents for the pinned Text revision')
}
const VERSION_COMPARISON_TESTS = '**/version-comparison.spec.ts'
const VIEWER_FALLBACK_TAG = /@viewer-fallback/

Expand All @@ -59,30 +63,31 @@ const webServer = externalServer
// Don't set `url` as it would take precedence over `wait.stdout` and tests start too early
// url: 'http://127.0.0.1:8089',
// Starts the Nextcloud docker container
command: 'node playwright/start-nextcloud-server.js',
// we use sigterm to notify the script to stop the container
// if it does not respond, we force kill it after 10 seconds
gracefulShutdown: {
signal: 'SIGTERM' as const,
timeout: 10000,
},
// `start-nextcloud-server.mjs` only starts the server if not reachable yet.
reuseExistingServer: false,
stderr: 'pipe' as const,
stdout: 'pipe' as const,
// max. 5 minutes for creating the container
timeout: 5 * 60 * 1000,
wait: {
command: 'node playwright/start-nextcloud-server.js',
// we use sigterm to notify the script to stop the container
// if it does not respond, we force kill it after 10 seconds
gracefulShutdown: {
signal: 'SIGTERM' as const,
timeout: 10000,
},
reuseExistingServer: false,
stderr: 'pipe' as const,
stdout: 'pipe' as const,
// Semantic tests also install dependencies and compile the pinned Text revision.
timeout: (SEMANTIC_E2E ? 10 : 5) * 60 * 1000,
wait: {
// we wait for this line to appear in the output of the webserver until consider it done
stdout: /Nextcloud is now ready to use/,
},
}
stdout: /Collectives test runtime ready/,
},
}

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './playwright',
outputDir: './test-results/browser',
metadata: { comparisonInitialView },
...(process.env.CI ? CI_CONFIG : LOCAL_CONFIG),
use: {
// Base URL to use in actions like `await page.goto('./')`.
Expand Down
39 changes: 23 additions & 16 deletions playwright/e2e/page-share.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { Locator } from '@playwright/test'
import type { APIRequestContext, Locator } from '@playwright/test'

import { runOcc } from '@nextcloud/e2e-test-server/docker'
import { expect, mergeTests } from '@playwright/test'
import { test as collectiveShareTest } from '../support/fixtures/collective-share.ts'
import { test as createCollectiveTest } from '../support/fixtures/create-collectives.ts'
Expand All @@ -14,6 +13,23 @@ import { ocsHeaders } from '../support/helpers/urls.ts'

const test = mergeTests(createCollectiveTest, pageSidebarTest, collectiveShareTest)

async function setPasswordProtection(request: APIRequestContext, enabled: boolean) {
const headers = { ...ocsHeaders, Authorization: `Basic ${Buffer.from('admin:admin').toString('base64')}` }
// HTTP writes invalidate the web server's config cache; CLI writes use a separate cache.
for (const key of ['shareapi_enable_link_password_by_default', 'shareapi_enforce_links_password']) {
await request.post(`/ocs/v2.php/apps/provisioning_api/api/v1/config/apps/core/${key}`, {
headers,
data: { value: enabled ? 'yes' : 'no' },
failOnStatusCode: true,
})
}
await request.fetch('/ocs/v2.php/cloud/apps/password_policy', {
method: enabled ? 'POST' : 'DELETE',
headers,
failOnStatusCode: true,
})
}

test.describe('Page share', () => {
test('Create share and open share unauthenticated', async ({ user, page, collective, pageSidebar, sharePage, sharePageList, shareEditor }) => {
const collectivePage = await collective.createPage({ title: 'Sharepage', content: 'Some content', user, page })
Expand Down Expand Up @@ -43,20 +59,15 @@ test.describe('Page share enforced password protection', () => {
let sharingTab: Locator
let shareActionsPanel: Locator

test.beforeAll(async () => {
await runOcc(['config:app:set', 'core', 'shareapi_enable_link_password_by_default', '--value', 'yes'])
await runOcc(['config:app:set', 'core', 'shareapi_enforce_links_password', '--value', 'yes'])
test.beforeAll(async ({ request }) => {
await setPasswordProtection(request, true)
})

test.afterAll(async () => {
await runOcc(['config:app:set', 'core', 'shareapi_enable_link_password_by_default', '--value', 'no'])
await runOcc(['config:app:set', 'core', 'shareapi_enforce_links_password', '--value', 'no'])
test.afterAll(async ({ request }) => {
await setPasswordProtection(request, false)
})

test.beforeEach(async ({ user, page, collective, pageSidebar }) => {
await runOcc(['app:enable', '--force', 'password_policy'])

// Dirty hack to wait for config changes to propagate through cache
await expect.poll(async () => {
const resp = await page.request.get('/ocs/v2.php/cloud/capabilities?format=json', { headers: ocsHeaders, failOnStatusCode: true })
const caps = await resp.json()
Expand All @@ -76,10 +87,6 @@ test.describe('Page share enforced password protection', () => {
await expect(shareActionsPanel.locator('input[autocomplete="new-password"]')).not.toHaveValue('')
})

test.afterEach(async () => {
await runOcc(['app:disable', 'password_policy'])
})

test('Create share and open share unauthenticated', async ({ page, collective, pageSidebar, sharePage, shareEditor }) => {
await shareActionsPanel.locator('input[autocomplete="new-password"]').fill('fiej2Ahl5pae')
await shareActionsPanel.getByRole('button', { name: 'Create share' }).click()
Expand All @@ -95,7 +102,7 @@ test.describe('Page share enforced password protection', () => {
await sharePage.goto(shares[0].getShareUrl())
await sharePage.getByRole('textbox', { name: 'Password' }).pressSequentially('fiej2Ahl5pae')
await sharePage.getByRole('button', { name: 'Submit' }).click()
await expect(shareEditor.getContent()).toBeVisible()
await shareEditor.getContent().waitFor({ state: 'visible' })
await expect(shareEditor.getContent()).toHaveText('Some content')
})

Expand Down
12 changes: 5 additions & 7 deletions playwright/e2e/version-comparison.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,14 @@ async function freshAuthenticatedContext(page: Page, serviceWorkers: 'allow' | '
}

test.describe('Version comparison route and current-byte contract', () => {
test('AUD-06 scheduled browser uses the compatible Text comparison API', async ({ user, page, collective }) => {
test('AUD-06 scheduled browser uses the compatible Text comparison API', async ({ user, page, collective }, testInfo) => {
const { dialog } = await openSeededComparison(collective, user, page, 'c599-e2e-text-api-page')
const initialTab = testInfo.config.metadata.comparisonInitialView === 'documents' ? 'Full documents' : 'Changes'
await expect(page.getByRole('tab', { name: initialTab, exact: true })).toHaveAttribute('aria-selected', 'true')
await expect(page.getByRole('tab', { name: 'Changes' })).toBeVisible()
await expect(page.getByRole('tab', { name: 'Full documents' })).toBeVisible()
const changesTab = page.locator('.text-comparison .view-tabs').getByRole('tab', { name: 'Changes' })
await changesTab.click()
const selectedBorderColor = await changesTab.evaluate((element) => getComputedStyle(element).borderBottomColor)
await changesTab.hover()
await expect(changesTab).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)')
Expand Down Expand Up @@ -226,10 +229,7 @@ test.describe('Version comparison route and current-byte contract', () => {
const collectivePage = await collective.createPage({ title: 'c599-e2e-comparison-page', user, page })
await seedVersionPair(collectivePage, user, page)
await collectivePage.open()
const sessionCreated = page.waitForResponse((response) => response.request().method() === 'PUT'
&& /\/apps\/text\/session\/.*\/create/.test(response.url()))
await collectivePage.switchMode(true)
await sessionCreated
await expect(page.locator('.text-menubar--ready')).toBeVisible()
editor.setMode(true)
const typedBytes = 'No-wait Playwright bytes 7f56c599'
Expand Down Expand Up @@ -380,6 +380,7 @@ test.describe('Version comparison route and current-byte contract', () => {

test('C16 rejects public comparison parameters without snapshot requests', async ({ user, page, collective }) => {
const collectivePage = await collective.createPage({ title: 'c599-e2e-public-comparison-page', user, page })
await seedVersionPair(collectivePage, user, page)
const share = await collective.createShare({ page })
try {
const snapshotRequests: string[] = []
Expand Down Expand Up @@ -425,10 +426,7 @@ test.describe('Version comparison route and current-byte contract', () => {
const collectivePage = await collective.createPage({ title: 'c599-e2e-committed-generation-page', user, page })
await seedVersionPair(collectivePage, user, page)
await collectivePage.open()
const sessionCreated = page.waitForResponse((response) => response.request().method() === 'PUT'
&& /\/apps\/text\/session\/.*\/create/.test(response.url()))
await collectivePage.switchMode(true)
await sessionCreated
await expect(page.locator('.text-menubar--ready')).toBeVisible()
editor.setMode(true)
const typedBytes = 'Committed generation bytes 7f56c599'
Expand Down
Loading
Loading