mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
| | PR | Base | Branch | | --- | --- | --- | --- | | 1 | #50476 | `master` | pre-existing correctness fixes | | 2 | #50413 | `fix/storage-explorer-listing-and-scroll` | `?path`/`?preview` deep-linking | | 3 | **this PR** | `feat/storage-nav-improvement` | end-to-end deep-link test | | 4 | #50480 | `test/storage-deep-link-e2e` | copy path / copy link row actions | ## What is the current behavior? The `?path` / `?preview` deep-linking added in #50413 has no end-to-end coverage. It is exercised by unit and component tests, but nothing verifies the real round trip through a browser and a live Storage backend. ## What is the new behavior? One spec that drills into a nested folder and asserts `?path`, opens a file and asserts `?preview`, reloads to confirm the deep link restores the same location rather than dropping back to the bucket root, then goes Back to confirm it walks up one level. Assertions go through `toHaveURL((url) => url.searchParams.get(...) === ...)` rather than a regex over the whole URL, so dots and other regex metacharacters in file names are compared literally. ## Additional context Kept on its own branch for one reason: **this spec has never been executed.** It was written without a local Supabase stack available to run it against, so its first real run is here in CI (`studio-e2e-test.yml`, `framework: [next, tanstack]` × 2 shards). Isolating it means iterating on it cannot hold up the feature work in #50413 below. Expect this one to need a pass or two. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DB6KEERERPWtLL4SY2RX4Q <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Improved end-to-end coverage for storage workflows, including bucket management, file and folder operations, uploads, downloads, search, and deep links. * Added reliable cleanup for test-created storage data, including when navigation, uploads, or assertions fail. * Continued verification of storage navigation and file and folder management behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Gildas Garcia <1122076+djhi@users.noreply.github.com>
645 lines
22 KiB
TypeScript
645 lines
22 KiB
TypeScript
import path from 'path'
|
|
import { expect } from '@playwright/test'
|
|
|
|
import { env } from '../env.config.js'
|
|
import { expectClipboardValue } from '../utils/clipboard.js'
|
|
import {
|
|
createBucket,
|
|
createFolder,
|
|
deleteBucket,
|
|
deleteItem,
|
|
downloadFile,
|
|
getStorageRowNameInput,
|
|
navigateToBucket,
|
|
navigateToStorageFiles,
|
|
renameItem,
|
|
uploadFile,
|
|
} from '../utils/storage-helpers.js'
|
|
import {
|
|
createBucket as createBucketViaApi,
|
|
deleteBucket as deleteBucketViaApi,
|
|
} from '../utils/storage/index.js'
|
|
import { test, withSetupCleanup } from '../utils/test.js'
|
|
import { waitForApiResponse } from '../utils/wait-for-response.js'
|
|
|
|
const bucketNamePrefix = 'pw_bucket'
|
|
|
|
test.describe('Storage', () => {
|
|
test.beforeEach(async ({ page, ref }) => {
|
|
await navigateToStorageFiles(page, ref)
|
|
})
|
|
|
|
test('can navigate to storage page', async ({ page, ref }) => {
|
|
await expect(
|
|
page.getByRole('button', { name: 'New bucket' }),
|
|
'New bucket button should be visible'
|
|
).toBeVisible()
|
|
|
|
// Verify we're on the storage files page
|
|
await expect(page).toHaveURL(new RegExp(`/project/${ref}/storage/files`))
|
|
})
|
|
|
|
test('can create a private bucket', async ({ page, ref }) => {
|
|
const bucketName = `${bucketNamePrefix}_private`
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
// Nothing
|
|
},
|
|
async () => {
|
|
await deleteBucketViaApi(bucketName)
|
|
}
|
|
)
|
|
await createBucket(page, ref, bucketName, false)
|
|
|
|
// Verify it's marked as private (no "Public" badge should be visible)
|
|
const bucketRow = page.getByRole('row').filter({ hasText: bucketName })
|
|
await expect(bucketRow, 'Bucket row should be visible').toBeVisible()
|
|
await expect(
|
|
bucketRow.getByText('Public'),
|
|
'Private bucket should not have Public badge'
|
|
).not.toBeVisible()
|
|
})
|
|
|
|
test('can create a public bucket', async ({ page, ref }) => {
|
|
const bucketName = `${bucketNamePrefix}_public`
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
// Nothing
|
|
},
|
|
async () => {
|
|
await deleteBucketViaApi(bucketName)
|
|
}
|
|
)
|
|
await createBucket(page, ref, bucketName, true)
|
|
|
|
// Verify it's marked as public - wait for the badge to appear
|
|
const bucketRow = page.getByRole('row').filter({ hasText: bucketName })
|
|
await expect(bucketRow, 'Bucket row should be visible').toBeVisible()
|
|
|
|
// The Public badge should be visible within the bucket row
|
|
await expect(
|
|
bucketRow.getByText('Public', { exact: true }),
|
|
'Bucket should be marked as Public'
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('can edit bucket settings', async ({ page, ref }) => {
|
|
const bucketName = `${bucketNamePrefix}_edit`
|
|
|
|
// Create a fresh private bucket via API
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createBucketViaApi(bucketName, false)
|
|
},
|
|
async () => {
|
|
await deleteBucketViaApi(bucketName)
|
|
}
|
|
)
|
|
await navigateToStorageFiles(page, ref)
|
|
|
|
// Navigate to the bucket
|
|
await navigateToBucket(page, ref, bucketName)
|
|
|
|
// Open edit bucket dropdown
|
|
await page.getByRole('button', { name: 'Edit bucket' }).click()
|
|
await page.getByRole('menuitem', { name: 'Bucket settings' }).click()
|
|
|
|
// Toggle public setting
|
|
const publicToggle = page.getByRole('switch', { name: 'Public bucket' })
|
|
await expect(publicToggle, 'Public toggle should be visible').toBeVisible()
|
|
await publicToggle.click()
|
|
|
|
// Save changes
|
|
const apiPromise = waitForApiResponse(page, 'storage', ref, `buckets/${bucketName}`, {
|
|
method: 'PATCH',
|
|
})
|
|
await page.getByRole('button', { name: 'Save' }).click()
|
|
await apiPromise
|
|
|
|
// Verify the bucket is now public
|
|
await expect(
|
|
page.getByText('Public').first(),
|
|
'Bucket should now be marked as Public'
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('can delete a bucket', async ({ page, ref }) => {
|
|
const bucketName = `${bucketNamePrefix}_delbkt`
|
|
|
|
// Create a bucket via API
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createBucketViaApi(bucketName, false)
|
|
},
|
|
async () => {
|
|
await deleteBucketViaApi(bucketName)
|
|
}
|
|
)
|
|
await navigateToStorageFiles(page, ref)
|
|
|
|
// Delete it via UI
|
|
await deleteBucket(page, ref, bucketName)
|
|
|
|
// Verify it's gone
|
|
await expect(
|
|
page.getByRole('row').filter({ hasText: bucketName }),
|
|
'Bucket should not be visible after deletion'
|
|
).not.toBeVisible()
|
|
})
|
|
|
|
test('can search for buckets', async ({ page, ref }) => {
|
|
const bucketName1 = `${bucketNamePrefix}_search_1`
|
|
const bucketName2 = `${bucketNamePrefix}_search_2`
|
|
|
|
// Create two buckets via API
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createBucketViaApi(bucketName1, false)
|
|
await createBucketViaApi(bucketName2, false)
|
|
},
|
|
async () => {
|
|
await deleteBucketViaApi(bucketName1)
|
|
await deleteBucketViaApi(bucketName2)
|
|
}
|
|
)
|
|
await navigateToStorageFiles(page, ref)
|
|
|
|
// Search for first bucket
|
|
const searchInput = page.getByPlaceholder('Search for a bucket')
|
|
await searchInput.fill('search_1')
|
|
|
|
// Verify only first bucket is visible
|
|
await expect(
|
|
page.getByRole('row').filter({ hasText: bucketName1 }),
|
|
'First bucket should be visible in search results'
|
|
).toBeVisible()
|
|
await expect(
|
|
page.getByRole('row').filter({ hasText: bucketName2 }),
|
|
'Second bucket should not be visible in search results'
|
|
).not.toBeVisible()
|
|
|
|
// Clear search
|
|
await searchInput.clear()
|
|
|
|
// Verify both buckets are visible
|
|
await expect(
|
|
page.getByRole('row').filter({ hasText: bucketName1 }),
|
|
'First bucket should be visible after clearing search'
|
|
).toBeVisible()
|
|
await expect(
|
|
page.getByRole('row').filter({ hasText: bucketName2 }),
|
|
'Second bucket should be visible after clearing search'
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('can upload a file', async ({ page, ref }) => {
|
|
const bucketName = `${bucketNamePrefix}_upload`
|
|
const fileName = 'test-file.txt'
|
|
|
|
// Create a bucket via API and navigate to it
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createBucketViaApi(bucketName, false)
|
|
},
|
|
async () => {
|
|
await deleteBucketViaApi(bucketName)
|
|
}
|
|
)
|
|
await navigateToStorageFiles(page, ref)
|
|
await navigateToBucket(page, ref, bucketName)
|
|
|
|
// Upload a file
|
|
const filePath = path.join(import.meta.dirname, 'files', fileName)
|
|
await uploadFile(page, filePath, fileName)
|
|
})
|
|
|
|
test('can create a folder', async ({ page, ref }) => {
|
|
const bucketName = `${bucketNamePrefix}_newfolder`
|
|
const folderName = 'test_folder'
|
|
|
|
// Create a bucket via API and navigate to it
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createBucketViaApi(bucketName, false)
|
|
},
|
|
async () => {
|
|
await deleteBucketViaApi(bucketName)
|
|
}
|
|
)
|
|
await navigateToStorageFiles(page, ref)
|
|
await navigateToBucket(page, ref, bucketName)
|
|
|
|
// Create a folder
|
|
await createFolder(page, folderName)
|
|
})
|
|
|
|
test('deep links to a nested folder and file via the URL', async ({ page, ref }) => {
|
|
const bucketName = `${bucketNamePrefix}_deeplink`
|
|
const folderName = 'deeplink_folder'
|
|
const fileName = 'test-file.txt'
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createBucketViaApi(bucketName, false)
|
|
},
|
|
async () => {
|
|
await deleteBucketViaApi(bucketName)
|
|
}
|
|
)
|
|
await navigateToStorageFiles(page, ref)
|
|
await navigateToBucket(page, ref, bucketName)
|
|
|
|
// Creating a folder drills into it, so the upload lands inside the folder
|
|
await createFolder(page, folderName)
|
|
await expect(page.getByText('Drop your files here')).toBeVisible()
|
|
const filePath = path.join(import.meta.dirname, 'files', fileName)
|
|
await uploadFile(page, filePath, fileName)
|
|
|
|
// Reload at the bucket root so opening the folder is a real navigation rather than
|
|
// the store drilling in on its own — only the former pushes a history entry
|
|
await navigateToStorageFiles(page, ref)
|
|
await navigateToBucket(page, ref, bucketName)
|
|
await page.getByTitle(folderName).click()
|
|
// Compare the parsed params rather than regex-matching the URL, so dots and other
|
|
// regex metacharacters in file names are treated literally
|
|
await expect(page).toHaveURL((url) => url.searchParams.get('path') === folderName)
|
|
|
|
// Opening the file records it in the URL alongside the folder
|
|
await page.getByTitle(fileName).click()
|
|
await expect(page).toHaveURL((url) => url.searchParams.get('path') === folderName)
|
|
await expect(page).toHaveURL((url) => url.searchParams.get('preview') === fileName)
|
|
|
|
// A reload restores the same location rather than dropping back to bucket root
|
|
const deepLink = page.url()
|
|
await page.reload()
|
|
await expect(
|
|
page.getByTitle(fileName),
|
|
'File should still be visible after reloading the deep link'
|
|
).toBeVisible()
|
|
expect(page.url()).toBe(deepLink)
|
|
|
|
// Back walks up out of the folder
|
|
await page.goBack()
|
|
await expect(page).toHaveURL((url) => url.searchParams.get('path') === null)
|
|
await expect(
|
|
page.getByTitle(folderName),
|
|
'Should be back at bucket root showing the folder'
|
|
).toBeVisible()
|
|
|
|
await deleteBucketViaApi(bucketName)
|
|
})
|
|
|
|
test('can rename a file', async ({ page, ref }) => {
|
|
const bucketName = `${bucketNamePrefix}_rename_file`
|
|
const fileName = 'test-file.txt'
|
|
const newFileName = 'renamed-file.txt'
|
|
|
|
// Create a bucket via API, navigate to it, and upload a file
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createBucketViaApi(bucketName, false)
|
|
},
|
|
async () => {
|
|
await deleteBucketViaApi(bucketName)
|
|
}
|
|
)
|
|
await navigateToStorageFiles(page, ref)
|
|
await navigateToBucket(page, ref, bucketName)
|
|
|
|
const filePath = path.join(import.meta.dirname, 'files', fileName)
|
|
await uploadFile(page, filePath, fileName)
|
|
|
|
// Rename the file
|
|
await renameItem(page, fileName, newFileName)
|
|
})
|
|
|
|
test('can rename a folder', async ({ page, ref }) => {
|
|
const bucketName = `${bucketNamePrefix}_mvdir`
|
|
const folderName = 'old_folder'
|
|
const newFolderName = 'new_folder'
|
|
|
|
// Create a bucket via API, navigate to it, and create a folder
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createBucketViaApi(bucketName, false)
|
|
},
|
|
async () => {
|
|
await deleteBucketViaApi(bucketName)
|
|
}
|
|
)
|
|
await navigateToStorageFiles(page, ref)
|
|
await navigateToBucket(page, ref, bucketName)
|
|
|
|
await createFolder(page, folderName)
|
|
|
|
// Rename the folder
|
|
await renameItem(page, folderName, newFolderName)
|
|
})
|
|
|
|
test('can copy a file url regardless of the opened folders', async ({ page, ref }) => {
|
|
const bucketName = `${bucketNamePrefix}_urls`
|
|
const folderName = 'test_folder'
|
|
const rootFileName = 'test-file.txt'
|
|
const rootFilePath = path.join(import.meta.dirname, 'files', rootFileName)
|
|
const folderFileName = 'test-file-2.txt'
|
|
const folderFilePath = path.join(import.meta.dirname, 'files', folderFileName)
|
|
|
|
// Create a bucket via API and navigate to it
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createBucketViaApi(bucketName, true)
|
|
},
|
|
async () => {
|
|
await deleteBucketViaApi(bucketName)
|
|
}
|
|
)
|
|
await navigateToStorageFiles(page, ref)
|
|
await navigateToBucket(page, ref, bucketName)
|
|
await uploadFile(page, rootFilePath, rootFileName)
|
|
// Create a folder
|
|
await createFolder(page, folderName)
|
|
// Wait for the folder file input
|
|
await expect(page.getByText('Drop your files here')).toBeVisible()
|
|
await uploadFile(page, folderFilePath, folderFileName)
|
|
|
|
// Right-click on the folder file to open context menu
|
|
const folderFile = page.getByTitle(folderFileName)
|
|
await folderFile.click({ button: 'right' })
|
|
await page.getByRole('menuitem', { name: 'Get URL' }).click()
|
|
await expectClipboardValue({
|
|
page,
|
|
value: `storage/v1/object/public/${bucketName}/${folderName}/${folderFileName}`,
|
|
})
|
|
await expect(page.getByRole('menuitem', { name: 'Get URL' })).not.toBeVisible()
|
|
|
|
// Right-click on the root file to open context menu while the folder is still open
|
|
const rootFile = page.getByTitle(rootFileName)
|
|
await rootFile.click({ button: 'right' })
|
|
await page.getByRole('menuitem', { name: 'Get URL' }).click()
|
|
await expectClipboardValue({
|
|
page,
|
|
value: `storage/v1/object/public/${bucketName}/${rootFileName}`,
|
|
})
|
|
await expect(page.getByRole('menuitem', { name: 'Get URL' })).not.toBeVisible()
|
|
|
|
// Click the actions button on the folder file to open dropdown menu
|
|
await page.getByRole('button', { name: `${folderFileName} actions` }).click()
|
|
await page.getByRole('menuitem', { name: 'Get URL' }).click()
|
|
await expectClipboardValue({
|
|
page,
|
|
value: `storage/v1/object/public/${bucketName}/${folderName}/${folderFileName}`,
|
|
})
|
|
await expect(page.getByRole('menuitem', { name: 'Get URL' })).not.toBeVisible()
|
|
|
|
// Click the actions button on the root file to open dropdown menu while the folder is still open
|
|
await page.getByRole('button', { name: `${rootFileName} actions` }).click()
|
|
await page.getByRole('menuitem', { name: 'Get URL' }).click()
|
|
await expectClipboardValue({
|
|
page,
|
|
value: `storage/v1/object/public/${bucketName}/${rootFileName}`,
|
|
})
|
|
await expect(page.getByRole('menuitem', { name: 'Get URL' })).not.toBeVisible()
|
|
|
|
// Click the folder file to open its preview pane
|
|
await folderFile.click()
|
|
await page.getByRole('button', { name: 'Get URL' }).click()
|
|
await expectClipboardValue({
|
|
page,
|
|
value: `storage/v1/object/public/${bucketName}/${folderName}/${folderFileName}`,
|
|
})
|
|
|
|
// Click the root file to open its preview pane while folder is still open
|
|
await rootFile.click()
|
|
await page.getByRole('button', { name: 'Get URL' }).click()
|
|
await expectClipboardValue({
|
|
page,
|
|
value: `storage/v1/object/public/${bucketName}/${rootFileName}`,
|
|
})
|
|
})
|
|
|
|
test('resets folder name when renaming with empty string', async ({ page, ref }) => {
|
|
const bucketName = `${bucketNamePrefix}_reset_enter`
|
|
const folderName = 'folder_to_rename'
|
|
|
|
// Create a bucket via API, navigate to it, and create a folder
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createBucketViaApi(bucketName, false)
|
|
},
|
|
async () => {
|
|
await deleteBucketViaApi(bucketName)
|
|
}
|
|
)
|
|
await navigateToStorageFiles(page, ref)
|
|
await navigateToBucket(page, ref, bucketName)
|
|
await createFolder(page, folderName)
|
|
|
|
// Right-click on the folder to open context menu
|
|
const folder = page.getByTitle(folderName)
|
|
await expect(folder, `Folder ${folderName} should be visible`).toBeVisible()
|
|
await folder.click({ button: 'right' })
|
|
|
|
// Click rename option from context menu
|
|
await page.getByRole('menuitem', { name: 'Rename' }).click()
|
|
|
|
// Clear the input and press Enter with empty name
|
|
const nameInput = getStorageRowNameInput(page)
|
|
await expect(nameInput, 'Rename input should be visible').toBeVisible()
|
|
await nameInput.clear()
|
|
await nameInput.press('Enter')
|
|
|
|
// Verify the input disappears (edit mode exits)
|
|
await expect(nameInput, 'Input should disappear after pressing Enter').not.toBeVisible()
|
|
|
|
// Verify the folder name is reset to original
|
|
await expect(
|
|
page.getByTitle(folderName),
|
|
'Folder should retain its original name'
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('resets folder name when clicking outside with empty string', async ({ page, ref }) => {
|
|
const bucketName = `${bucketNamePrefix}_reset_blur`
|
|
const folderName = 'folder_to_blur'
|
|
|
|
// Create a bucket via API, navigate to it, and create a folder
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createBucketViaApi(bucketName, false)
|
|
},
|
|
async () => {
|
|
await deleteBucketViaApi(bucketName)
|
|
}
|
|
)
|
|
await navigateToStorageFiles(page, ref)
|
|
await navigateToBucket(page, ref, bucketName)
|
|
await createFolder(page, folderName)
|
|
|
|
// Right-click on the folder to open context menu
|
|
const folder = page.getByTitle(folderName)
|
|
await expect(folder, `Folder ${folderName} should be visible`).toBeVisible()
|
|
await folder.click({ button: 'right' })
|
|
|
|
// Click rename option from context menu
|
|
await page.getByRole('menuitem', { name: 'Rename' }).click()
|
|
|
|
// Clear the input and click outside to blur
|
|
const nameInput = getStorageRowNameInput(page)
|
|
await expect(nameInput, 'Rename input should be visible').toBeVisible()
|
|
await nameInput.clear()
|
|
|
|
// Click outside the input to trigger blur
|
|
await page.getByRole('button', { name: 'Edit bucket' }).click()
|
|
|
|
// Verify the folder name is reset to original
|
|
await expect(
|
|
page.getByTitle(folderName),
|
|
'Folder should retain its original name after blur'
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('resets storage view when switching buckets', async ({ page, ref }) => {
|
|
const bucketName = `${bucketNamePrefix}_navigation`
|
|
const bucketName2 = `${bucketNamePrefix}2_navigation`
|
|
const folderName = 'folder_navigation'
|
|
const fileName = 'test-file.txt'
|
|
|
|
// Create 2 bucket via API, navigate to the first
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createBucketViaApi(bucketName, false)
|
|
await createBucketViaApi(bucketName2, false)
|
|
},
|
|
async () => {
|
|
await deleteBucketViaApi(bucketName)
|
|
await deleteBucketViaApi(bucketName2)
|
|
}
|
|
)
|
|
await navigateToStorageFiles(page, ref)
|
|
await navigateToBucket(page, ref, bucketName)
|
|
|
|
// create a folder and add a file
|
|
await createFolder(page, folderName)
|
|
// Open the folder
|
|
await page.getByTitle(folderName).click()
|
|
const filePath = path.join(import.meta.dirname, 'files', fileName)
|
|
await uploadFile(page, filePath, fileName)
|
|
|
|
// Navigate to bucket list
|
|
await page.getByRole('link', { name: 'Files' }).nth(1).click()
|
|
// Navigate to the 2nd bucket
|
|
await navigateToBucket(page, ref, bucketName2)
|
|
await expect(page.getByTitle(fileName)).not.toBeVisible()
|
|
})
|
|
|
|
test('can delete a file', async ({ page, ref }) => {
|
|
const bucketName = `${bucketNamePrefix}_delete_file`
|
|
const fileName = 'test-file.txt'
|
|
|
|
// Create a bucket via API, navigate to it, and upload a file
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createBucketViaApi(bucketName, false)
|
|
},
|
|
async () => {
|
|
await deleteBucketViaApi(bucketName)
|
|
}
|
|
)
|
|
await navigateToStorageFiles(page, ref)
|
|
await navigateToBucket(page, ref, bucketName)
|
|
|
|
const filePath = path.join(import.meta.dirname, 'files', fileName)
|
|
await uploadFile(page, filePath, fileName)
|
|
|
|
// Delete the file
|
|
await deleteItem(page, fileName)
|
|
})
|
|
|
|
test('can delete a folder', async ({ page, ref }) => {
|
|
const bucketName = `${bucketNamePrefix}_delete_folder`
|
|
const folderName = 'test_folder'
|
|
|
|
// Create a bucket via API, navigate to it, and create a folder
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createBucketViaApi(bucketName, false)
|
|
},
|
|
async () => {
|
|
await deleteBucketViaApi(bucketName)
|
|
}
|
|
)
|
|
await navigateToStorageFiles(page, ref)
|
|
await navigateToBucket(page, ref, bucketName)
|
|
await createFolder(page, folderName)
|
|
|
|
// Delete the folder
|
|
await deleteItem(page, folderName)
|
|
})
|
|
|
|
test('can download a file', async ({ page, ref }) => {
|
|
const bucketName = `${bucketNamePrefix}_download`
|
|
const fileName = 'test-file.txt'
|
|
|
|
// Create a bucket via API, navigate to it, and upload a file
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createBucketViaApi(bucketName, false)
|
|
},
|
|
async () => {
|
|
await deleteBucketViaApi(bucketName)
|
|
}
|
|
)
|
|
await navigateToStorageFiles(page, ref)
|
|
await navigateToBucket(page, ref, bucketName)
|
|
|
|
const filePath = path.join(import.meta.dirname, 'files', fileName)
|
|
await uploadFile(page, filePath, fileName)
|
|
|
|
// Download the file
|
|
await downloadFile(page, fileName)
|
|
})
|
|
})
|
|
|
|
test.describe('Storage Settings - Self Hosted', () => {
|
|
test.skip(env.IS_PLATFORM, 'Storage settings are only disabled on self-hosted')
|
|
|
|
test('settings tab should not be visible in navigation', async ({ page, ref }) => {
|
|
// Navigate to storage files page
|
|
await page.goto(`/project/${ref}/storage/files`)
|
|
|
|
// Wait for the page to load
|
|
await expect(
|
|
page.getByRole('button', { name: 'New bucket' }),
|
|
'New bucket button should be visible'
|
|
).toBeVisible()
|
|
|
|
// Verify Buckets and Policies tabs are visible but Settings is not
|
|
// Use href patterns to avoid matching other "Settings" links in the sidebar
|
|
await expect(
|
|
page.getByRole('link', { name: 'Buckets' }).filter({ hasText: /^Buckets$/ }),
|
|
'Buckets tab should be visible'
|
|
).toBeVisible()
|
|
await expect(
|
|
page.getByRole('link', { name: 'Policies' }).filter({ hasText: /^Policies$/ }),
|
|
'Policies tab should be visible'
|
|
).toBeVisible()
|
|
await expect(
|
|
page.locator(`a[href="/project/${ref}/storage/files/settings"]`),
|
|
'Settings tab should NOT be visible for self-hosted'
|
|
).not.toBeVisible()
|
|
})
|
|
|
|
test('direct navigation to settings page should show error', async ({ page, ref }) => {
|
|
// Navigate directly to the settings page
|
|
await page.goto(`/project/${ref}/storage/files/settings`)
|
|
|
|
// Should show an error message indicating settings are not available
|
|
await expect(
|
|
page.getByText('Storage settings are not available for self-hosted projects'),
|
|
'Error message should be visible'
|
|
).toBeVisible()
|
|
})
|
|
})
|