Skip to content

Commit 0e2bbdc

Browse files
test(archive): add e2e tests for archive and unarchive flows
AI-assistant: Claude Code 2.1.101 (Claude Sonnet 4.6) Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
1 parent d6bc6bd commit 0e2bbdc

2 files changed

Lines changed: 174 additions & 35 deletions

File tree

playwright/e2e/tables-archive.spec.ts

Lines changed: 173 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,67 +3,206 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6+
import { type Page } from '@playwright/test'
67
import { test, expect } from '../support/fixtures'
78
import { ocsRequest } from '../support/api'
9+
import { createContext, ensureNavigationOpen } from '../support/commands'
810

9-
test.describe('Archive tables/views', () => {
11+
// ---------------------------------------------------------------------------
12+
// Helpers
13+
// ---------------------------------------------------------------------------
1014

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 } }) => {
1232
await page.goto('/index.php/apps/tables')
1333

1434
const tutorialTable = page.locator('[data-cy="navigationTableItem"]').first()
15-
1635
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)
2138

2239
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+
)
2443
await page.getByText('Archive table').click({ force: true })
2544

26-
const archiveRequest = await archiveTableReqPromise
45+
const archiveRequest = await archiveReqPromise
2746
expect(archiveRequest.status()).toBe(200)
28-
const body = await archiveRequest.json()
29-
expect(body.ocs.data.archived).toBe(true)
3047

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 })
3251
})
3352

34-
test('can unarchive tables', async ({ userPage: { page, user } }) => {
53+
test('can unarchive a table via the archived section menu', async ({ userPage: { page, user } }) => {
3554
test.setTimeout(60000)
3655
await page.goto('/index.php/apps/tables')
3756

3857
const tutorialTable = page.locator('[data-cy="navigationTableItem"]').filter({ hasText: 'Welcome to Nextcloud Tables!' }).first()
3958
const tutorialHref = await tutorialTable.locator('a').first().getAttribute('href')
4059
const tableId = tutorialHref?.match(/\/table\/(\d+)/)?.[1]
41-
42-
await expect(tutorialTable).toContainText('Welcome to Nextcloud Tables!')
4360
expect(tableId).toBeTruthy()
4461

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+
})
5067

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`,
63144
})
64-
expect(unarchiveResponse.ok()).toBeTruthy()
65145

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 } }) => {
66174
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)
68207
})
69208
})

src/store/store.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export const useTablesStore = defineStore('store', {
126126
},
127127
setContext(context) {
128128
const index = this.contexts.findIndex(c => c.id === context.id)
129-
this.contexts[index] = context
129+
this.contexts.splice(index, 1, context)
130130
},
131131
setActiveRowId(rowId) {
132132
this.activeRowId = rowId

0 commit comments

Comments
 (0)