|
3 | 3 | * SPDX-License-Identifier: AGPL-3.0-or-later |
4 | 4 | */ |
5 | 5 |
|
| 6 | +import { type Page } from '@playwright/test' |
6 | 7 | import { test, expect } from '../support/fixtures' |
7 | 8 | import { ocsRequest } from '../support/api' |
| 9 | +import { createContext, ensureNavigationOpen } from '../support/commands' |
8 | 10 |
|
9 | | -test.describe('Archive tables/views', () => { |
| 11 | +// --------------------------------------------------------------------------- |
| 12 | +// Helpers |
| 13 | +// --------------------------------------------------------------------------- |
10 | 14 |
|
11 | | - test('can archive tables', async ({ userPage: { page } }) => { |
| 15 | +async function openNavItemMenu(page: Page, itemLocator: ReturnType<Page['locator']>) { |
| 16 | + await ensureNavigationOpen(page) |
| 17 | + await itemLocator.waitFor({ state: 'visible', timeout: 10000 }) |
| 18 | + await itemLocator.scrollIntoViewIfNeeded() |
| 19 | + await itemLocator.hover() |
| 20 | + const menuButton = itemLocator.getByRole('button', { name: /Actions|Open menu/i }).first() |
| 21 | + await menuButton.waitFor({ state: 'visible', timeout: 5000 }) |
| 22 | + await menuButton.click({ force: true }) |
| 23 | +} |
| 24 | + |
| 25 | +// --------------------------------------------------------------------------- |
| 26 | +// Table archive |
| 27 | +// --------------------------------------------------------------------------- |
| 28 | + |
| 29 | +test.describe('Archive tables', () => { |
| 30 | + |
| 31 | + test('can archive a table via the navigation menu', async ({ userPage: { page } }) => { |
12 | 32 | await page.goto('/index.php/apps/tables') |
13 | 33 |
|
14 | 34 | const tutorialTable = page.locator('[data-cy="navigationTableItem"]').first() |
15 | | - |
16 | 35 | await expect(tutorialTable).toContainText('Welcome to Nextcloud Tables!') |
17 | | - await tutorialTable.hover() |
18 | | - const menuButton = tutorialTable.locator('[aria-haspopup="menu"]').first() |
19 | | - await menuButton.waitFor({ state: 'visible' }) |
20 | | - await menuButton.click({ force: true }) |
| 36 | + |
| 37 | + await openNavItemMenu(page, tutorialTable) |
21 | 38 |
|
22 | 39 | await page.getByText('Archive table').waitFor({ state: 'visible' }) |
23 | | - const archiveTableReqPromise = page.waitForResponse(r => r.url().includes('/apps/tables/api/2/tables/') && r.request().method() === 'PUT') |
| 40 | + const archiveReqPromise = page.waitForResponse( |
| 41 | + r => r.url().includes('/apps/tables/api/2/tables/') && r.url().endsWith('/archive') && r.request().method() === 'POST', |
| 42 | + ) |
24 | 43 | await page.getByText('Archive table').click({ force: true }) |
25 | 44 |
|
26 | | - const archiveRequest = await archiveTableReqPromise |
| 45 | + const archiveRequest = await archiveReqPromise |
27 | 46 | expect(archiveRequest.status()).toBe(200) |
28 | | - const body = await archiveRequest.json() |
29 | | - expect(body.ocs.data.archived).toBe(true) |
30 | 47 |
|
31 | | - await expect(tutorialTable.locator('..').locator('..')).toContainText('Archived tables') |
| 48 | + // Table must be gone from the main list and the archived section must appear |
| 49 | + await expect(tutorialTable).not.toBeVisible() |
| 50 | + await expect(page.getByText('Archived tables')).toBeVisible({ timeout: 10000 }) |
32 | 51 | }) |
33 | 52 |
|
34 | | - test('can unarchive tables', async ({ userPage: { page, user } }) => { |
| 53 | + test('can unarchive a table via the archived section menu', async ({ userPage: { page, user } }) => { |
35 | 54 | test.setTimeout(60000) |
36 | 55 | await page.goto('/index.php/apps/tables') |
37 | 56 |
|
38 | 57 | const tutorialTable = page.locator('[data-cy="navigationTableItem"]').filter({ hasText: 'Welcome to Nextcloud Tables!' }).first() |
39 | 58 | const tutorialHref = await tutorialTable.locator('a').first().getAttribute('href') |
40 | 59 | const tableId = tutorialHref?.match(/\/table\/(\d+)/)?.[1] |
41 | | - |
42 | | - await expect(tutorialTable).toContainText('Welcome to Nextcloud Tables!') |
43 | 60 | expect(tableId).toBeTruthy() |
44 | 61 |
|
45 | | - // Archive it first so we can unarchive |
46 | | - await tutorialTable.hover() |
47 | | - const menuButtonArchive = tutorialTable.locator('[aria-haspopup="menu"]').first() |
48 | | - await menuButtonArchive.waitFor({ state: 'visible' }) |
49 | | - await menuButtonArchive.click({ force: true }) |
| 62 | + // Archive via API so we start from a clean known state |
| 63 | + await ocsRequest(page.request, user, { |
| 64 | + method: 'POST', |
| 65 | + url: `/ocs/v2.php/apps/tables/api/2/tables/${tableId}/archive?format=json`, |
| 66 | + }) |
50 | 67 |
|
51 | | - await page.getByText('Archive table').waitFor({ state: 'visible' }) |
52 | | - const archiveReqPromise = page.waitForResponse(r => r.url().includes('/apps/tables/api/2/tables/') && r.request().method() === 'PUT') |
53 | | - await page.getByText('Archive table').click({ force: true }) |
54 | | - await archiveReqPromise |
55 | | - |
56 | | - // Wait for navigation to reflect the archived state. |
57 | | - const archivedTablesToggle = page.getByRole('link', { name: 'Archived tables' }) |
58 | | - await expect(archivedTablesToggle).toBeVisible({ timeout: 10000 }) |
59 | | - const unarchiveResponse = await ocsRequest(page.request, user, { |
60 | | - method: 'PUT', |
61 | | - url: `/ocs/v2.php/apps/tables/api/2/tables/${tableId}?format=json`, |
62 | | - data: { archived: false }, |
| 68 | + await page.reload({ waitUntil: 'domcontentloaded' }) |
| 69 | + await ensureNavigationOpen(page) |
| 70 | + |
| 71 | + // Expand the archived section by clicking its collapse toggle (.collapse is NcAppNavigationItem's toggle class) |
| 72 | + const archivedSection = page.locator('li').filter({ hasText: 'Archived tables' }).first() |
| 73 | + await archivedSection.waitFor({ state: 'visible', timeout: 10000 }) |
| 74 | + await archivedSection.locator('.collapse').first().click() |
| 75 | + |
| 76 | + // Find the archived table and unarchive it |
| 77 | + const archivedTable = page.locator('[data-cy="navigationTableItem"]').filter({ hasText: 'Welcome to Nextcloud Tables!' }).first() |
| 78 | + await archivedTable.waitFor({ state: 'visible', timeout: 10000 }) |
| 79 | + await openNavItemMenu(page, archivedTable) |
| 80 | + |
| 81 | + const unarchiveReqPromise = page.waitForResponse( |
| 82 | + r => r.url().includes('/apps/tables/api/2/tables/') && r.url().endsWith('/archive') && r.request().method() === 'DELETE', |
| 83 | + ) |
| 84 | + await page.getByText('Unarchive table').click({ force: true }) |
| 85 | + |
| 86 | + const unarchiveRequest = await unarchiveReqPromise |
| 87 | + expect(unarchiveRequest.status()).toBe(200) |
| 88 | + |
| 89 | + // Table reappears in the main list |
| 90 | + await expect( |
| 91 | + page.locator('[data-cy="navigationTableItem"] a[title="Welcome to Nextcloud Tables!"]').first(), |
| 92 | + ).toBeVisible({ timeout: 10000 }) |
| 93 | + }) |
| 94 | +}) |
| 95 | + |
| 96 | +// --------------------------------------------------------------------------- |
| 97 | +// Context (application) archive |
| 98 | +// --------------------------------------------------------------------------- |
| 99 | + |
| 100 | +test.describe('Archive applications', () => { |
| 101 | + |
| 102 | + test('can archive an application via the navigation menu', async ({ userPage: { page } }) => { |
| 103 | + await page.goto('/index.php/apps/tables') |
| 104 | + const contextTitle = 'archive-test-app' |
| 105 | + await createContext(page, contextTitle) |
| 106 | + |
| 107 | + const contextItem = page.locator('[data-cy="navigationContextItem"]').filter({ hasText: contextTitle }).first() |
| 108 | + await openNavItemMenu(page, contextItem) |
| 109 | + |
| 110 | + await page.getByText('Archive application').waitFor({ state: 'visible' }) |
| 111 | + const archiveReqPromise = page.waitForResponse( |
| 112 | + r => r.url().includes('/apps/tables/api/2/contexts/') && r.url().endsWith('/archive') && r.request().method() === 'POST', |
| 113 | + ) |
| 114 | + await page.getByText('Archive application').click({ force: true }) |
| 115 | + |
| 116 | + const archiveRequest = await archiveReqPromise |
| 117 | + expect(archiveRequest.status()).toBe(200) |
| 118 | + |
| 119 | + // "Archived applications" section must appear |
| 120 | + await expect( |
| 121 | + page.locator('li').filter({ hasText: 'Archived applications' }).first(), |
| 122 | + ).toBeVisible({ timeout: 10000 }) |
| 123 | + // Item must appear exactly once (inside the archived section) — if still in the active list the count would be 2 |
| 124 | + await expect( |
| 125 | + page.locator('[data-cy="navigationContextItem"]').filter({ hasText: contextTitle }), |
| 126 | + ).toHaveCount(1, { timeout: 5000 }) |
| 127 | + }) |
| 128 | + |
| 129 | + test('can unarchive an application via the archived section', async ({ userPage: { page, user } }) => { |
| 130 | + await page.goto('/index.php/apps/tables') |
| 131 | + const contextTitle = 'unarchive-test-app' |
| 132 | + await createContext(page, contextTitle) |
| 133 | + |
| 134 | + // Read context ID from the navigation link |
| 135 | + const contextItem = page.locator('[data-cy="navigationContextItem"]').filter({ hasText: contextTitle }).first() |
| 136 | + const contextHref = await contextItem.locator('a').first().getAttribute('href') |
| 137 | + const contextId = contextHref?.match(/\/application\/(\d+)/)?.[1] |
| 138 | + expect(contextId).toBeTruthy() |
| 139 | + |
| 140 | + // Archive via API for a clean starting state |
| 141 | + await ocsRequest(page.request, user, { |
| 142 | + method: 'POST', |
| 143 | + url: `/ocs/v2.php/apps/tables/api/2/contexts/${contextId}/archive?format=json`, |
63 | 144 | }) |
64 | | - expect(unarchiveResponse.ok()).toBeTruthy() |
65 | 145 |
|
| 146 | + await page.reload({ waitUntil: 'domcontentloaded' }) |
| 147 | + await ensureNavigationOpen(page) |
| 148 | + |
| 149 | + // Expand the archived applications section by clicking its collapse toggle |
| 150 | + const archivedSection = page.locator('li').filter({ hasText: 'Archived applications' }).first() |
| 151 | + await archivedSection.waitFor({ state: 'visible', timeout: 10000 }) |
| 152 | + await archivedSection.locator('.collapse').first().click() |
| 153 | + |
| 154 | + // Find the archived context item and unarchive it |
| 155 | + const archivedContextItem = page.locator('[data-cy="navigationContextItem"]').filter({ hasText: contextTitle }).first() |
| 156 | + await archivedContextItem.waitFor({ state: 'visible', timeout: 10000 }) |
| 157 | + await openNavItemMenu(page, archivedContextItem) |
| 158 | + |
| 159 | + const unarchiveReqPromise = page.waitForResponse( |
| 160 | + r => r.url().includes('/apps/tables/api/2/contexts/') && r.url().endsWith('/archive') && r.request().method() === 'DELETE', |
| 161 | + ) |
| 162 | + await page.getByText('Unarchive application').click({ force: true }) |
| 163 | + |
| 164 | + const unarchiveRequest = await unarchiveReqPromise |
| 165 | + expect(unarchiveRequest.status()).toBe(200) |
| 166 | + |
| 167 | + // Application reappears in the main (active) list |
| 168 | + const activeContextItem = page.locator('[data-cy="navigationContextItem"]').filter({ hasText: contextTitle }).first() |
| 169 | + await expect(activeContextItem).toBeVisible({ timeout: 10000 }) |
| 170 | + await expect(page.locator('li').filter({ hasText: 'Archived applications' })).toHaveCount(0) |
| 171 | + }) |
| 172 | + |
| 173 | + test('archived application appears in the sidebar archived section and not in the main list', async ({ userPage: { page, user } }) => { |
66 | 174 | await page.goto('/index.php/apps/tables') |
67 | | - await expect(page.locator('[data-cy="navigationTableItem"] a[title="Welcome to Nextcloud Tables!"]').first()).toBeVisible({ timeout: 10000 }) |
| 175 | + const contextTitle = 'sidebar-section-test-app' |
| 176 | + await createContext(page, contextTitle) |
| 177 | + |
| 178 | + const contextItem = page.locator('[data-cy="navigationContextItem"]').filter({ hasText: contextTitle }).first() |
| 179 | + const contextHref = await contextItem.locator('a').first().getAttribute('href') |
| 180 | + const contextId = contextHref?.match(/\/application\/(\d+)/)?.[1] |
| 181 | + expect(contextId).toBeTruthy() |
| 182 | + |
| 183 | + await ocsRequest(page.request, user, { |
| 184 | + method: 'POST', |
| 185 | + url: `/ocs/v2.php/apps/tables/api/2/contexts/${contextId}/archive?format=json`, |
| 186 | + }) |
| 187 | + |
| 188 | + await page.reload({ waitUntil: 'domcontentloaded' }) |
| 189 | + await ensureNavigationOpen(page) |
| 190 | + |
| 191 | + // The "Archived applications" collapsible section must be present |
| 192 | + const archivedSection = page.locator('li').filter({ hasText: 'Archived applications' }).first() |
| 193 | + await expect(archivedSection).toBeVisible({ timeout: 10000 }) |
| 194 | + |
| 195 | + // Expand it by clicking the collapse toggle |
| 196 | + await archivedSection.locator('.collapse').first().click() |
| 197 | + const archivedItem = page.locator('[data-cy="navigationContextItem"]').filter({ hasText: contextTitle }).first() |
| 198 | + await expect(archivedItem).toBeVisible({ timeout: 10000 }) |
| 199 | + |
| 200 | + // The item must NOT appear in the active Applications section above |
| 201 | + // (count inside the collapsed archived NcAppNavigationItem should be exactly 1, |
| 202 | + // and the active list should have 0 matching items — verified by checking |
| 203 | + // there's no second occurrence) |
| 204 | + await expect( |
| 205 | + page.locator('[data-cy="navigationContextItem"]').filter({ hasText: contextTitle }), |
| 206 | + ).toHaveCount(1) |
68 | 207 | }) |
69 | 208 | }) |
0 commit comments