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
88 changes: 88 additions & 0 deletions playwright/e2e/navigation-load-order.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { APIRequestContext } from '@playwright/test'
import { test, expect } from '../support/fixtures'
import { createRandomUser, ocsRequest, type TestUser } from '../support/api'
import {
createTable,
createTextLineColumn,
createView,
ensureNavigationOpen,
getNavigationNodeId,
loadTableListingLast,
} from '../support/commands'
import { login } from '../support/login'

async function deleteTable(request: APIRequestContext, owner: TestUser, tableId: number) {
await ocsRequest(request, owner, {
method: 'DELETE',
url: `/ocs/v2.php/apps/tables/api/2/tables/${tableId}?format=json`,
})
}

async function shareWithUser(
request: APIRequestContext,
owner: TestUser,
nodeType: 'view',
nodeId: number,
receiver: TestUser,
) {
await ocsRequest(request, owner, {
method: 'POST',
// the api/1 routes are plain app routes, not OCS ones
url: '/index.php/apps/tables/api/1/shares',
data: {
nodeId,
nodeType,
receiver: receiver.userId,
receiverType: 'user',
permissionRead: true,
},
})
}

test.describe('Navigation with a slow table listing', () => {
const cleanUpTasks: Array<() => Promise<unknown>> = []

test.afterEach(async () => {
// keep the test server reusable, the tests run against a shared instance
// (the users stay, the fixture owns their lifecycle)
while (cleanUpTasks.length > 0) {
await cleanUpTasks.pop()?.()
}
})

test('shows a view shared with me', async ({ userPage: { page, user: owner }, request }) => {
const recipient = await createRandomUser(request)
const tableTitle = 'Table of ' + owner.userId
const viewTitle = 'Shared view'

await page.goto('/index.php/apps/tables')
await createTable(page, tableTitle)
// the table actions, and with them the create view action, need a column to show up
await createTextLineColumn(page, 'What', '', '', true)
await createView(page, viewTitle)
// deleting the table takes its views and shares with it
const tableId = await getNavigationNodeId(page, 'table', tableTitle)
cleanUpTasks.push(() => deleteTable(request, owner, tableId))
const viewId = await getNavigationNodeId(page, 'view', viewTitle)
await shareWithUser(request, owner, 'view', viewId, recipient)

await page.context().clearCookies()
await login(page, recipient)

// the shared views answer before the table listing, which used to drop them
const { answered: sharedViewsAnswered } = await loadTableListingLast(page, '/apps/tables/view')
await page.goto('/index.php/apps/tables')
await sharedViewsAnswered
await ensureNavigationOpen(page)

await expect(
page.locator(`[data-cy="navigationViewItem"] a[title="${viewTitle}"]`),
).toBeVisible()
})

})
58 changes: 58 additions & 0 deletions playwright/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,64 @@
}
}

/**
* Reads the id of a navigation entry from the link it points to.
*
* @param page the page showing the navigation
* @param nodeType the kind of entry to look up
* @param title the title of the entry
* @return the id of the table or view
*/
export async function getNavigationNodeId(page: Page, nodeType: 'table' | 'view', title: string) {
await ensureNavigationOpen(page)

const entry = page
.locator(`[data-cy="navigation${nodeType === 'table' ? 'Table' : 'View'}Item"] a[title="${title}"]`)
.first()

await entry.waitFor({ state: 'visible', timeout: ACTION_TIMEOUT })
const id = (await entry.getAttribute('href'))?.match(new RegExp(`/${nodeType}/(\\d+)`))?.[1]
expect(id, `Could not read the id of ${nodeType} "${title}" from the navigation`).toBeTruthy()

return Number(id)
}

/**
* Holds the table listing back until the given request has been answered.
*
* The navigation loads tables, views shared with me and applications in parallel.
* Use this to pin down the order in which the table listing is applied to the store.
* Await the returned `answered` to make sure the awaited request really happened,
* otherwise the listing is let through after the timeout and nothing is pinned down.
*
* @param page the page to intercept the requests of
* @param until url fragment of the request that has to answer first
* @param timeout how long to wait for that request at most
* @return `answered` resolves once the awaited request has been answered
*/
export async function loadTableListingLast(page: Page, until: string, timeout = ACTION_TIMEOUT) {
let release = () => {}
const released = new Promise<void>((resolve) => {
release = resolve
})
const expired = new Promise<void>((resolve) => {
setTimeout(resolve, timeout)
})

page.on('response', (response) => {
if (response.url().includes(until)) {
release()
}
})

await page.route('**/apps/tables/table', async (route) => {
await Promise.race([released, expired])
await route.continue()
})

return { answered: released }
}

export async function ensureNavigationOpen(page: Page) {
const openButton = page.getByRole('button', { name: /open navigation/i }).first()
if (await openButton.isVisible().catch(() => false)) {
Expand Down Expand Up @@ -322,7 +380,7 @@
.getByRole('button', { name: /^Create application$/ })
.first()
.click({ force: true })
await expect(page.locator('[data-cy="createContextModal"]')).toBeVisible()

Check failure on line 383 in playwright/support/commands.ts

View workflow job for this annotation

GitHub Actions / Playwright (stable34)

[chromium] › playwright/e2e/column-relation.spec.ts:98:2 › Test column relation › Relation labels render inside an application context

1) [chromium] › playwright/e2e/column-relation.spec.ts:98:2 › Test column relation › Relation labels render inside an application context Error: expect(locator).toBeVisible() failed Locator: locator('[data-cy="createContextModal"]') Expected: visible Timeout: 30000ms Error: element(s) not found Call log: - Expect "toBeVisible" with timeout 30000ms - waiting for locator('[data-cy="createContextModal"]') at support/commands.ts:383 381 | .first() 382 | .click({ force: true }) > 383 | await expect(page.locator('[data-cy="createContextModal"]')).toBeVisible() | ^ 384 | 385 | const input = page.locator('[data-cy="createContextTitle"]') 386 | await input.clear() at createContext (/home/runner/work/tables/tables/playwright/support/commands.ts:383:63) at /home/runner/work/tables/tables/playwright/e2e/column-relation.spec.ts:149:3

Check failure on line 383 in playwright/support/commands.ts

View workflow job for this annotation

GitHub Actions / Playwright (master)

[chromium] › playwright/e2e/column-relation.spec.ts:98:2 › Test column relation › Relation labels render inside an application context

2) [chromium] › playwright/e2e/column-relation.spec.ts:98:2 › Test column relation › Relation labels render inside an application context Error: expect(locator).toBeVisible() failed Locator: locator('[data-cy="createContextModal"]') Expected: visible Timeout: 30000ms Error: element(s) not found Call log: - Expect "toBeVisible" with timeout 30000ms - waiting for locator('[data-cy="createContextModal"]') at support/commands.ts:383 381 | .first() 382 | .click({ force: true }) > 383 | await expect(page.locator('[data-cy="createContextModal"]')).toBeVisible() | ^ 384 | 385 | const input = page.locator('[data-cy="createContextTitle"]') 386 | await input.clear() at createContext (/home/runner/actions-runner/_work/tables/tables/playwright/support/commands.ts:383:63) at /home/runner/actions-runner/_work/tables/tables/playwright/e2e/column-relation.spec.ts:149:3

Check failure on line 383 in playwright/support/commands.ts

View workflow job for this annotation

GitHub Actions / Playwright (stable33)

[chromium] › playwright/e2e/column-relation.spec.ts:98:2 › Test column relation › Relation labels render inside an application context

1) [chromium] › playwright/e2e/column-relation.spec.ts:98:2 › Test column relation › Relation labels render inside an application context Error: expect(locator).toBeVisible() failed Locator: locator('[data-cy="createContextModal"]') Expected: visible Timeout: 30000ms Error: element(s) not found Call log: - Expect "toBeVisible" with timeout 30000ms - waiting for locator('[data-cy="createContextModal"]') at support/commands.ts:383 381 | .first() 382 | .click({ force: true }) > 383 | await expect(page.locator('[data-cy="createContextModal"]')).toBeVisible() | ^ 384 | 385 | const input = page.locator('[data-cy="createContextTitle"]') 386 | await input.clear() at createContext (/home/runner/actions-runner/_work/tables/tables/playwright/support/commands.ts:383:63) at /home/runner/actions-runner/_work/tables/tables/playwright/e2e/column-relation.spec.ts:149:3

const input = page.locator('[data-cy="createContextTitle"]')
await input.clear()
Expand Down
24 changes: 17 additions & 7 deletions src/store/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,7 @@
try {
const res = await axios.get(generateUrl('/apps/tables/table'))
this.setTables(res.data)
let views = []
res.data.forEach(table => {
if (table.views) {
views = views.concat(table.views)
}
})
this.setViews(views)
this.replaceViewsOfListedTables(res.data)
} catch (e) {
displayError(e, t('tables', 'Could not load tables.'))
showError(t('tables', 'Could not fetch tables'))
Expand All @@ -168,6 +162,22 @@
return true
},

/**

Check warning on line 165 in src/store/store.js

View workflow job for this annotation

GitHub Actions / NPM lint

Missing JSDoc @PARAM "tables" declaration
* We have two sources of views, this coming from tables endpoint, and those
* that were shared directly or through contexts.
*/
replaceViewsOfListedTables(tables) {
const tableIdsShippingViews = new Set(
tables
.filter(table => !table.isShared || table.onSharePermissions?.manage)
.map(table => table.id),
)
const viewsOfOtherTables = this.views.filter(view => !tableIdsShippingViews.has(view.tableId))
const viewsOfListedTables = tables.flatMap(table => table.views ?? [])

this.setViews([...viewsOfListedTables, ...viewsOfOtherTables])
},

async loadViewsSharedWithMeFromBE() {
this.setLoading({ key: 'viewsShared', value: true })

Expand Down
Loading