From 09f36316a7e6cee40a5ccaacaefe07fb4e9fd627 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Fri, 18 Sep 2026 10:28:45 +0200 Subject: [PATCH] test(studio): cover storage explorer deep links end to end (#50478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit | | 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 ## 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. --------- Co-authored-by: Claude Co-authored-by: Gildas Garcia <1122076+djhi@users.noreply.github.com> --- e2e/studio/features/storage.spec.ts | 224 +++++++++++++++++++++++----- 1 file changed, 189 insertions(+), 35 deletions(-) diff --git a/e2e/studio/features/storage.spec.ts b/e2e/studio/features/storage.spec.ts index 8c98463cd24..a6acb389cd3 100644 --- a/e2e/studio/features/storage.spec.ts +++ b/e2e/studio/features/storage.spec.ts @@ -19,7 +19,7 @@ import { createBucket as createBucketViaApi, deleteBucket as deleteBucketViaApi, } from '../utils/storage/index.js' -import { test } from '../utils/test.js' +import { test, withSetupCleanup } from '../utils/test.js' import { waitForApiResponse } from '../utils/wait-for-response.js' const bucketNamePrefix = 'pw_bucket' @@ -42,7 +42,14 @@ test.describe('Storage', () => { test('can create a private bucket', async ({ page, ref }) => { const bucketName = `${bucketNamePrefix}_private` - await deleteBucketViaApi(bucketName) + 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) @@ -57,7 +64,14 @@ test.describe('Storage', () => { test('can create a public bucket', async ({ page, ref }) => { const bucketName = `${bucketNamePrefix}_public` - await deleteBucketViaApi(bucketName) + 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 @@ -75,8 +89,14 @@ test.describe('Storage', () => { const bucketName = `${bucketNamePrefix}_edit` // Create a fresh private bucket via API - await deleteBucketViaApi(bucketName) - await createBucketViaApi(bucketName, false) + await using _ = await withSetupCleanup( + async () => { + await createBucketViaApi(bucketName, false) + }, + async () => { + await deleteBucketViaApi(bucketName) + } + ) await navigateToStorageFiles(page, ref) // Navigate to the bucket @@ -109,8 +129,14 @@ test.describe('Storage', () => { const bucketName = `${bucketNamePrefix}_delbkt` // Create a bucket via API - await deleteBucketViaApi(bucketName) - await createBucketViaApi(bucketName, false) + await using _ = await withSetupCleanup( + async () => { + await createBucketViaApi(bucketName, false) + }, + async () => { + await deleteBucketViaApi(bucketName) + } + ) await navigateToStorageFiles(page, ref) // Delete it via UI @@ -128,10 +154,16 @@ test.describe('Storage', () => { const bucketName2 = `${bucketNamePrefix}_search_2` // Create two buckets via API - await deleteBucketViaApi(bucketName1) - await deleteBucketViaApi(bucketName2) - await createBucketViaApi(bucketName1, false) - await createBucketViaApi(bucketName2, false) + 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 @@ -167,8 +199,14 @@ test.describe('Storage', () => { const fileName = 'test-file.txt' // Create a bucket via API and navigate to it - await deleteBucketViaApi(bucketName) - await createBucketViaApi(bucketName, false) + await using _ = await withSetupCleanup( + async () => { + await createBucketViaApi(bucketName, false) + }, + async () => { + await deleteBucketViaApi(bucketName) + } + ) await navigateToStorageFiles(page, ref) await navigateToBucket(page, ref, bucketName) @@ -182,8 +220,14 @@ test.describe('Storage', () => { const folderName = 'test_folder' // Create a bucket via API and navigate to it - await deleteBucketViaApi(bucketName) - await createBucketViaApi(bucketName, false) + await using _ = await withSetupCleanup( + async () => { + await createBucketViaApi(bucketName, false) + }, + async () => { + await deleteBucketViaApi(bucketName) + } + ) await navigateToStorageFiles(page, ref) await navigateToBucket(page, ref, bucketName) @@ -191,14 +235,76 @@ test.describe('Storage', () => { 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 deleteBucketViaApi(bucketName) - await createBucketViaApi(bucketName, false) + await using _ = await withSetupCleanup( + async () => { + await createBucketViaApi(bucketName, false) + }, + async () => { + await deleteBucketViaApi(bucketName) + } + ) await navigateToStorageFiles(page, ref) await navigateToBucket(page, ref, bucketName) @@ -215,8 +321,14 @@ test.describe('Storage', () => { const newFolderName = 'new_folder' // Create a bucket via API, navigate to it, and create a folder - await deleteBucketViaApi(bucketName) - await createBucketViaApi(bucketName, false) + await using _ = await withSetupCleanup( + async () => { + await createBucketViaApi(bucketName, false) + }, + async () => { + await deleteBucketViaApi(bucketName) + } + ) await navigateToStorageFiles(page, ref) await navigateToBucket(page, ref, bucketName) @@ -235,8 +347,14 @@ test.describe('Storage', () => { const folderFilePath = path.join(import.meta.dirname, 'files', folderFileName) // Create a bucket via API and navigate to it - await deleteBucketViaApi(bucketName) - await createBucketViaApi(bucketName, true) + 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) @@ -306,8 +424,14 @@ test.describe('Storage', () => { const folderName = 'folder_to_rename' // Create a bucket via API, navigate to it, and create a folder - await deleteBucketViaApi(bucketName) - await createBucketViaApi(bucketName, false) + 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) @@ -341,8 +465,14 @@ test.describe('Storage', () => { const folderName = 'folder_to_blur' // Create a bucket via API, navigate to it, and create a folder - await deleteBucketViaApi(bucketName) - await createBucketViaApi(bucketName, false) + 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) @@ -377,10 +507,16 @@ test.describe('Storage', () => { const fileName = 'test-file.txt' // Create 2 bucket via API, navigate to the first - await deleteBucketViaApi(bucketName) - await deleteBucketViaApi(bucketName2) - await createBucketViaApi(bucketName, false) - await createBucketViaApi(bucketName2, false) + 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) @@ -403,8 +539,14 @@ test.describe('Storage', () => { const fileName = 'test-file.txt' // Create a bucket via API, navigate to it, and upload a file - await deleteBucketViaApi(bucketName) - await createBucketViaApi(bucketName, false) + await using _ = await withSetupCleanup( + async () => { + await createBucketViaApi(bucketName, false) + }, + async () => { + await deleteBucketViaApi(bucketName) + } + ) await navigateToStorageFiles(page, ref) await navigateToBucket(page, ref, bucketName) @@ -420,8 +562,14 @@ test.describe('Storage', () => { const folderName = 'test_folder' // Create a bucket via API, navigate to it, and create a folder - await deleteBucketViaApi(bucketName) - await createBucketViaApi(bucketName, false) + 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) @@ -435,8 +583,14 @@ test.describe('Storage', () => { const fileName = 'test-file.txt' // Create a bucket via API, navigate to it, and upload a file - await deleteBucketViaApi(bucketName) - await createBucketViaApi(bucketName, false) + await using _ = await withSetupCleanup( + async () => { + await createBucketViaApi(bucketName, false) + }, + async () => { + await deleteBucketViaApi(bucketName) + } + ) await navigateToStorageFiles(page, ref) await navigateToBucket(page, ref, bucketName)