Skip to content
Merged
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
70 changes: 48 additions & 22 deletions playwright/e2e/basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

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

import { expect, test } from '@playwright/test'
import { login } from '../support/login.ts'
import { currentNoteId, newNoteButton, waitForNoteRoute } from '../support/note.ts'
import { NoteEditor } from '../support/sections/NoteEditor.ts'

function currentNoteId(page: Page): number | null {
const match = page.url().match(/\/note\/(\d+)(?:\?.*)?$/)
return match ? Number(match[1]) : null
}

async function waitForNoteRoute(page: Page, previousNoteId: number | null): Promise<number> {
await expect.poll(() => currentNoteId(page)).not.toBe(previousNoteId)

const noteId = currentNoteId(page)
if (noteId === null || noteId === previousNoteId) {
throw new Error('Expected to navigate to a note route')
}

return noteId
}

test.describe('Basic checks', () => {
test.beforeEach(async ({ page }) => {
await login(page)
Expand All @@ -38,9 +21,8 @@ test.describe('Basic checks', () => {
test('Create note and type', async ({ page }) => {
await page.goto('/index.php/apps/notes/')
const previousNoteId = currentNoteId(page)
const newNoteButton = page.getByRole('button', { name: 'New note', exact: true })
await expect(newNoteButton).toBeVisible()
await newNoteButton.click()
await expect(newNoteButton(page)).toBeVisible()
await newNoteButton(page).click()
await waitForNoteRoute(page, previousNoteId)

const editor = new NoteEditor(page)
Expand All @@ -51,7 +33,7 @@ test.describe('Basic checks', () => {
test('Open share sidebar from note actions', async ({ page }) => {
await page.goto('/index.php/apps/notes/')
const previousNoteId = currentNoteId(page)
await page.getByRole('button', { name: 'New note', exact: true }).click()
await newNoteButton(page).click()
const noteId = await waitForNoteRoute(page, previousNoteId)

const editor = new NoteEditor(page)
Expand All @@ -68,4 +50,48 @@ test.describe('Basic checks', () => {
await expect(page.locator('[data-cy-notes-share-sidebar]')).toBeVisible({ timeout: 15000 })
await expect(page.getByText('Internal shares')).toBeVisible({ timeout: 15000 })
})

test('persists note content after a reload', async ({ page }) => {
await page.goto('/index.php/apps/notes/')
const previousNoteId = currentNoteId(page)
await newNoteButton(page).click()
const noteId = await waitForNoteRoute(page, previousNoteId)

const editor = new NoteEditor(page)
const content = `Persistence check ${Date.now()}`

const savedResponse = page.waitForResponse((response) => response.url().includes(`/notes/${noteId}`)
&& response.request().method() === 'PUT')
await editor.type(content)
await savedResponse

await page.reload()
await expect(page).toHaveURL(new RegExp(`/note/${noteId}(\\?.*)?$`))
await editor.expectText(content)
})

test('filters notes with the search field', async ({ page }) => {
await page.goto('/index.php/apps/notes/')

const uniqueWord = `Findme${Date.now()}`
const previousNoteId = currentNoteId(page)
await newNoteButton(page).click()
const noteId = await waitForNoteRoute(page, previousNoteId)

const editor = new NoteEditor(page)
await editor.type(uniqueWord)
await editor.expectText(uniqueWord)

const noteLink = page.locator(`a[href$="/note/${noteId}"]`).first()

const searchField = page.getByRole('textbox', { name: 'Search for notes', exact: true })
await searchField.fill('this text matches no note at all')
await expect(noteLink).toHaveCount(0)

await searchField.fill(uniqueWord)
await expect(noteLink).toBeVisible()

await searchField.fill('')
await expect(noteLink).toBeVisible()
})
})
61 changes: 30 additions & 31 deletions playwright/e2e/category-actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { Locator, Page, TestInfo } from '@playwright/test'

import { expect, test } from '@playwright/test'
import { login } from '../support/login.ts'
import { createNote, currentNoteId, newNoteButton, noteRow, uniqueTitle, waitForNoteRoute } from '../support/note.ts'

function appNavigation(page: Page): Locator {
return page.getByRole('navigation').filter({
Expand All @@ -18,10 +19,6 @@ function newCategoryButton(page: Page): Locator {
return page.getByRole('button', { name: 'New category', exact: true })
}

function contentNewNoteButton(page: Page): Locator {
return page.getByRole('button', { name: 'New note', exact: true })
}

function notesSearchField(page: Page): Locator {
return page.getByRole('textbox', { name: 'Search for notes', exact: true })
}
Expand All @@ -46,20 +43,11 @@ async function expectNavigationItemActive(page: Page, name: string): Promise<voi
await expect(navigationLink(page, name)).toHaveAttribute('aria-current', 'page')
}

function uniqueCategoryName(prefix: string, testInfo: TestInfo): string {
return `Playwright ${prefix} ${testInfo.parallelIndex}-${Date.now()}`
}

function currentNoteId(page: Page): number | null {
const match = page.url().match(/\/note\/(\d+)(?:\?.*)?$/)
return match ? Number(match[1]) : null
}

async function openNotesApp(page: Page): Promise<void> {
await page.goto('/index.php/apps/notes/')
await expect(newCategoryButton(page)).toBeVisible()
await expect(contentNewNoteButton(page)).toHaveCount(1)
await expect(contentNewNoteButton(page)).toBeVisible()
await expect(newNoteButton(page)).toHaveCount(1)
await expect(newNoteButton(page)).toBeVisible()
}

async function createCategory(page: Page, name: string): Promise<void> {
Expand All @@ -76,25 +64,14 @@ async function createCategory(page: Page, name: string): Promise<void> {
await expectNavigationItemActive(page, name)
}

async function waitForNewNoteRoute(page: Page, previousNoteId: number | null): Promise<number> {
await expect.poll(() => currentNoteId(page)).not.toBe(previousNoteId)

const noteId = currentNoteId(page)
if (noteId === null || noteId === previousNoteId) {
throw new Error('Expected a new note route after creating a note')
}

return noteId
}

async function ensureNotesView(page: Page): Promise<void> {
if (await notesSearchField(page).isVisible()) {
return
}

const previousNoteId = currentNoteId(page)
await contentNewNoteButton(page).click()
await waitForNewNoteRoute(page, previousNoteId)
await newNoteButton(page).click()
await waitForNoteRoute(page, previousNoteId)
await expect(notesSearchField(page)).toBeVisible()
}

Expand All @@ -104,7 +81,7 @@ async function createNoteInSelectedCategory(page: Page, category: string): Promi
const previousNoteId = currentNoteId(page)
await notesViewNewNoteButton(page).click()

const noteId = await waitForNewNoteRoute(page, previousNoteId)
const noteId = await waitForNoteRoute(page, previousNoteId)
await expect(navigationRow(page, category).locator('.app-navigation-entry__counter-wrapper')).toContainText('1')

return noteId
Expand All @@ -128,7 +105,7 @@ test.describe('Category actions', () => {
})

test('renames a category from the actions menu', async ({ page }, testInfo: TestInfo) => {
const category = uniqueCategoryName('rename', testInfo)
const category = uniqueTitle('rename', testInfo)
const renamedCategory = `${category} renamed`
const noteId = await (async () => {
await createCategory(page, category)
Expand All @@ -151,7 +128,7 @@ test.describe('Category actions', () => {
})

test('deletes a category from the actions menu', async ({ page }, testInfo: TestInfo) => {
const category = uniqueCategoryName('delete', testInfo)
const category = uniqueTitle('delete', testInfo)
await createCategory(page, category)
const noteId = await createNoteInSelectedCategory(page, category)
const deletedNoteUrl = new RegExp(`/note/${noteId}(\\?.*)?$`)
Expand All @@ -169,3 +146,25 @@ test.describe('Category actions', () => {
await expect(page).not.toHaveURL(deletedNoteUrl)
})
})

test.describe('Drag and drop', () => {
test.beforeEach(async ({ page }) => {
await login(page)
await page.goto('/index.php/apps/notes/')
await expect(newCategoryButton(page)).toBeVisible()
})

test('moves a note into a category by dragging it', async ({ page }, testInfo: TestInfo) => {
const category = uniqueTitle('drag-target', testInfo)
const title = uniqueTitle('drag-note', testInfo)

await createCategory(page, category)
const noteId = await createNote(page, title)

await noteRow(page, noteId).dragTo(navigationRow(page, category))

await expect(navigationRow(page, category).locator('.app-navigation-entry__counter-wrapper')).toContainText('1')
await navigationRow(page, category).getByRole('link').click()
await expect(noteRow(page, noteId)).toBeVisible()
})
})
80 changes: 80 additions & 0 deletions playwright/e2e/note-actions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

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

import { expect, test } from '@playwright/test'
import { login } from '../support/login.ts'
import { createNote, newNoteButton, noteRow, uniqueTitle } from '../support/note.ts'

async function openNoteActions(page: Page, noteId: number): Promise<Locator> {
const row = noteRow(page, noteId)
await row.hover()
await row.locator('.action-item__menutoggle').click()
return row
}

test.describe('Note actions', () => {
test.beforeEach(async ({ page }) => {
await login(page)
await page.goto('/index.php/apps/notes/')
await expect(newNoteButton(page)).toBeVisible()
})

test('toggles favorite from the actions menu', async ({ page }, testInfo: TestInfo) => {
const title = uniqueTitle('favorite', testInfo)
const noteId = await createNote(page, title)

await openNoteActions(page, noteId)
await page.getByRole('menuitem', { name: 'Add to favorites' }).click()

await openNoteActions(page, noteId)
await expect(page.getByRole('menuitem', { name: 'Remove from favorites' })).toBeVisible()
await page.keyboard.press('Escape')

await openNoteActions(page, noteId)
await page.getByRole('menuitem', { name: 'Remove from favorites' }).click()

await openNoteActions(page, noteId)
await expect(page.getByRole('menuitem', { name: 'Add to favorites' })).toBeVisible()
})

test('renames a note from the actions menu', async ({ page }, testInfo: TestInfo) => {
const title = uniqueTitle('rename', testInfo)
const renamedTitle = `${title} renamed`
const noteId = await createNote(page, title)

await openNoteActions(page, noteId)
await page.getByRole('menuitem', { name: 'Rename' }).click()

const renameInput = page.getByRole('dialog', { name: 'Actions' }).getByRole('textbox')
await expect(renameInput).toBeVisible()
await renameInput.fill(renamedTitle)
await renameInput.press('Enter')

await expect(noteRow(page, noteId)).toContainText(renamedTitle)
})

test('deletes a note and undoes the deletion', async ({ page }, testInfo: TestInfo) => {
const title = uniqueTitle('delete', testInfo)
const autotitleSettled = page.waitForResponse((response) => response.url().includes('/autotitle')).catch(() => null)
const noteId = await createNote(page, title)
await autotitleSettled

await openNoteActions(page, noteId)
await page.getByRole('menuitem', { name: 'Delete note' }).click()

await expect(noteRow(page, noteId)).toHaveCount(0)

const undoButton = page.getByRole('button', { name: 'Undo Delete', exact: true })
await expect(undoButton).toBeVisible()

const undoRequest = page.waitForResponse((response) => response.url().includes('/notes/undo') && response.request().method() === 'POST')
await undoButton.click()
await undoRequest

await expect(page.getByRole('link', { name: title })).toBeVisible()
})
})
50 changes: 50 additions & 0 deletions playwright/support/note.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

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

import { expect } from '@playwright/test'
import { NoteEditor } from './sections/NoteEditor.ts'

export function uniqueTitle(prefix: string, testInfo: TestInfo): string {
return `Playwright ${prefix} ${testInfo.parallelIndex}-${Date.now()}`
}

export function currentNoteId(page: Page): number | null {
const match = page.url().match(/\/note\/(\d+)(?:\?.*)?$/)
return match ? Number(match[1]) : null
}

export function newNoteButton(page: Page): Locator {
return page.getByRole('button', { name: 'New note', exact: true })
}

export function noteRow(page: Page, noteId: number): Locator {
return page.locator(`a[href$="/note/${noteId}"]`).first()
.locator('xpath=ancestor::li[1]')
}

export async function waitForNoteRoute(page: Page, previousNoteId: number | null): Promise<number> {
await expect.poll(() => currentNoteId(page)).not.toBe(previousNoteId)

const noteId = currentNoteId(page)
if (noteId === null || noteId === previousNoteId) {
throw new Error('Expected to navigate to a note route')
}

return noteId
}

export async function createNote(page: Page, title: string): Promise<number> {
const previousNoteId = currentNoteId(page)
await newNoteButton(page).click()
const noteId = await waitForNoteRoute(page, previousNoteId)

const editor = new NoteEditor(page)
await editor.type(title)
await editor.expectText(title)

return noteId
}
Loading