Skip to content

Commit

Permalink
tests: Use POM for testing
Browse files Browse the repository at this point in the history
Page object models allow to write tests independent of locators,
this allows for easier maintenance in case locators change.

Signed-off-by: Ferdinand Thiessen <[email protected]>
  • Loading branch information
susnux committed Jun 13, 2024
1 parent f32ab90 commit d378258
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 72 deletions.
90 changes: 32 additions & 58 deletions playwright/e2e/create-empty-form.spec.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,58 @@
/**
* @copyright Copyright (c) 2024 Ferdinand Thiessen <[email protected]>
*
* @author Ferdinand Thiessen <[email protected]>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-FileCopyrightText: 2024 Ferdinand Thiessen <[email protected]>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { expect } from '@playwright/test'
import { test } from '../fixtures/random-user'

import { expect, mergeTests } from '@playwright/test'
import { test as randomUserTest } from '../support/fixtures/random-user'
import { test as appNavigationTest } from '../support/fixtures/navigation'
import { test as formTest } from '../support/fixtures/form'

const test = mergeTests(randomUserTest, appNavigationTest, formTest)

test.beforeEach(async ({ page }) => {
await page.goto('apps/forms')
await page.waitForURL(/apps\/forms$/)
})

test('It shows the empty content', async ({ page }) => {
await expect(page.getByText('No forms created yet')).toBeVisible()
await expect(page.getByRole('button', { name: 'Create a form' })).toBeVisible()
})
test.describe('No forms created - empty content', () => {
test('It shows the empty content', async ({ page }) => {
await expect(page.getByText('No forms created yet')).toBeVisible()
await expect(
page.getByRole('button', { name: 'Create a form' }),
).toBeVisible()
})

test('Use button to create new form', async ({ page }) => {
await page.getByRole('button', { name: 'Create a form' }).click()
test('Use button to create new form', async ({ page, appNavigation }) => {
const oldNumber = (await appNavigation.ownFormsLocator.all()).length

await page.waitForURL(/apps\/forms\/.+/)
await page.getByRole('button', { name: 'Create a form' }).click()
await page.waitForURL(/apps\/forms\/.+/)

// check we are in edit mode by default and the heading is focussed
await expect(page.locator('h2 textarea')).toBeVisible()
await expect(page.locator('h2 textarea')).toBeFocused()
const newNumber = (await appNavigation.ownFormsLocator.all()).length
expect(newNumber - oldNumber).toBe(1)
})
})

test('Use app navigation to create new form', async ({ page }) => {
await page.getByRole('navigation')
.getByRole('button', { name: 'New form' })
.click()

await page.waitForURL(/apps\/forms\/.+/)
test('Use app navigation to create new form', async ({ appNavigation, form }) => {
await appNavigation.clickNewForm()

// check we are in edit mode by default and the heading is focussed
await expect(page.locator('h2 textarea')).toBeVisible()
await expect(page.locator('h2 textarea')).toBeFocused()
await expect(form.titleField).toBeFocused()
})

test('Form name updated in navigation', async ({ page }) => {
// Create a form
await page
.getByRole('navigation')
.getByRole('button', { name: 'New form' })
.click()

await page.waitForURL(/apps\/forms\/.+/)
test('Form name updated in navigation', async ({ appNavigation, form }) => {
await appNavigation.clickNewForm()

// check we are in edit mode by default and the heading is focussed
await expect(page.locator('h2 textarea')).toBeVisible()
await expect(page.locator('h2 textarea')).toBeFocused()
await expect(form.titleField).toBeFocused()

// check the form exists in the navigation
await page
.getByRole('list', { name: 'Your forms' })
.getByRole('link', { name: 'New form' })
.isVisible()
await expect(appNavigation.getOwnForm('New form')).toBeVisible()

// Update the title
await page.locator('h2 textarea').fill('My example form')
await form.fillTitle('My example form')

// See the title is updated
await page
.getByRole('list', { name: 'Your forms' })
.getByRole('link', { name: 'My example form' })
.isVisible()
await expect(appNavigation.getOwnForm('My example form')).toBeVisible()
})
18 changes: 18 additions & 0 deletions playwright/support/fixtures/form.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* SPDX-FileCopyrightText: 2024 Ferdinand Thiessen <[email protected]>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { test as baseTest } from '@playwright/test'
import { FormSection } from '../sections/FormSection'

interface FormFixture {
form: FormSection
}

export const test = baseTest.extend<FormFixture>({
form: async ({ page }, use) => {
const form = new FormSection(page)
await use(form)
},
})
18 changes: 18 additions & 0 deletions playwright/support/fixtures/navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* SPDX-FileCopyrightText: 2024 Ferdinand Thiessen <[email protected]>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { test as baseTest } from '@playwright/test'
import { AppNavigationSection } from '../sections/AppNavigationSection'

interface AppNavigationFixture {
appNavigation: AppNavigationSection
}

export const test = baseTest.extend<AppNavigationFixture>({
appNavigation: async ({ page }, use) => {
const appNavigation = new AppNavigationSection(page)
await use(appNavigation)
},
})
43 changes: 43 additions & 0 deletions playwright/support/sections/AppNavigationSection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* SPDX-FileCopyrightText: 2024 Ferdinand Thiessen <[email protected]>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

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

export class AppNavigationSection {
public readonly navigationLocator: Locator
public readonly newFormLocator: Locator
public readonly ownFormsLocator: Locator
public readonly sharedFormsLocator: Locator

// eslint-disable-next-line no-useless-constructor
constructor(public readonly page: Page) {
this.navigationLocator = this.page.getByRole('navigation', {
name: 'Forms navigation',
})
this.newFormLocator = this.navigationLocator.getByRole('button', {
name: 'New form',
})
this.ownFormsLocator = this.navigationLocator
.getByRole('list', { name: 'Your forms' })
.getByRole('listitem')
this.sharedFormsLocator = this.navigationLocator
.getByRole('button', { name: 'Shared forms' })
.getByRole('listitem')
}

public async clickNewForm(): Promise<void> {
await this.newFormLocator.click()
}

public async openArchivedForms(): Promise<void> {
await this.navigationLocator
.getByRole('button', { name: 'Archived forms' })
.click()
}

public getOwnForm(name: string | RegExp): Locator {
return this.ownFormsLocator.getByRole('link', { name })
}
}
23 changes: 23 additions & 0 deletions playwright/support/sections/FormSection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* SPDX-FileCopyrightText: 2024 Ferdinand Thiessen <[email protected]>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

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

export class FormSection {
public readonly mainContent: Locator
public readonly titleField: Locator

// eslint-disable-next-line no-useless-constructor
constructor(public readonly page: Page) {
this.mainContent = this.page.getByRole('main')
this.titleField = this.mainContent.getByRole('textbox', {
name: 'Form title',
})
}

public async fillTitle(text: string): Promise<void> {
await this.titleField.fill(text)
}
}
43 changes: 29 additions & 14 deletions playwright/support/utils/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,36 @@ import { expect, type APIRequestContext } from '@playwright/test'
* @param options.user User to use for executing the command
* @param options.rejectOnError Reject the returned promise in case of non-zero exit code
*/
export async function runShell(command: string, options?: { user?: string, rejectOnError?: boolean, env?: Record<string, string|number>}) {
export async function runShell(
command: string,
options?: {
user?: string
rejectOnError?: boolean
env?: Record<string, string | number>
},
) {
const containerName = 'nextcloud-cypress-tests_forms'
const container = docker.getContainer(containerName)

const exec = await container.exec({
Cmd: ['sh', '-c', command],
Env: Object.entries(options?.env ?? {}).map(([name, value]) => `${name}=${value}`),
Env: Object.entries(options?.env ?? {}).map(
([name, value]) => `${name}=${value}`,
),
User: options?.user,
AttachStderr: true,
AttachStdout: true,
})

const stream = await exec.start({ })
const stream = await exec.start({})
return new Promise((resolve, reject) => {
let data = ''
stream.on('data', (chunk: string) => { data += chunk })
stream.on('data', (chunk: string) => {
data += chunk
})
stream.on('error', (error: unknown) => reject(error))
stream.on('end', async () => {
const inspect = await exec.inspect({ })
const inspect = await exec.inspect({})
if (options?.rejectOnError !== false && inspect.ExitCode) {
reject(data)
} else {
Expand All @@ -49,14 +60,14 @@ export async function runShell(command: string, options?: { user?: string, rejec
* @param options.env Process environment to pass
* @param options.rejectOnError Reject the returned promise in case of non-zero exit code
*/
export async function runOCC(command: string, options?: { env?: Record<string, string|number>, rejectOnError?: boolean }) {
return await runShell(
`php ./occ ${command}`,
{
...options,
user: 'www-data',
},
)
export async function runOCC(
command: string,
options?: { env?: Record<string, string | number>; rejectOnError?: boolean },
) {
return await runShell(`php ./occ ${command}`, {
...options,
user: 'www-data',
})
}

/**
Expand All @@ -72,7 +83,11 @@ export function restoreDatabase() {
* @param user The username to login
* @param password The password to login
*/
export async function login(request: APIRequestContext, user: string, password: string) {
export async function login(
request: APIRequestContext,
user: string,
password: string,
) {
const tokenResponse = await request.get('./csrftoken')
const requesttoken = (await tokenResponse.json()).token

Expand Down

0 comments on commit d378258

Please sign in to comment.