From b9cd17230354d9fbcb1a24d5855d3c8c1cfc6306 Mon Sep 17 00:00:00 2001 From: Imants Date: Thu, 23 Jul 2026 19:33:57 +0300 Subject: [PATCH 01/14] feat: show cloud loading status notices --- .../ManageMenu/CommunityCloud/CloudSearch.tsx | 28 +++-- .../CommunityCloud/WithCloudSearchContext.tsx | 14 ++- .../common/LoadingStatusNotices.tsx | 40 +++++++ .../code-snippets-community-featured.spec.ts | 107 ++++++++++++------ 4 files changed, 142 insertions(+), 47 deletions(-) create mode 100644 src/js/components/common/LoadingStatusNotices.tsx diff --git a/src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx b/src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx index 2810b01f1..2a10f1359 100644 --- a/src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx +++ b/src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx @@ -7,6 +7,7 @@ import { REST_BASES } from '../../../utils/restAPI' import { isLicensed } from '../../../utils/screen' import { isProSnippet } from '../../../utils/snippets/snippets' import { TableNav } from '../../common/ListTable/TableNavigation' +import { LoadingStatusNotices } from '../../common/LoadingStatusNotices' import { SnippetViewToggle } from '../../common/SnippetViewToggle' import { CloudSnippetsTable } from './CloudSnippetsTable' import { CloudSnippetAuthor, SearchResult } from './SearchResult' @@ -191,18 +192,7 @@ const SearchResultsTable: React.FC = ({ snippetView, set } const SearchResults: React.FC = ({ snippetView, setSnippetView }) => { - const { searchResults, searchParams, isErrored } = useCloudSearch() - - if (isErrored) { - return ( -
-

{__( - 'An error occurred while fetching search results. Please try again.', - 'code-snippets' - )}

-
- ) - } + const { searchResults, searchParams } = useCloudSearch() if (!searchResults) { return null @@ -229,6 +219,19 @@ const SearchResults: React.FC = ({ snippetView, setSnipp : null } +const SearchStatus = () => { + const { isErrored, isLoading } = useCloudSearch() + + return isErrored || isLoading + ? + : null +} + export interface CloudSearchProps { snippetView: SnippetView setSnippetView: (view: SnippetView) => void @@ -237,5 +240,6 @@ export interface CloudSearchProps { export const CloudSearch: React.FC = ({ snippetView, setSnippetView }) =>
+
diff --git a/src/js/components/ManageMenu/CommunityCloud/WithCloudSearchContext.tsx b/src/js/components/ManageMenu/CommunityCloud/WithCloudSearchContext.tsx index 20a541284..118e8ae96 100644 --- a/src/js/components/ManageMenu/CommunityCloud/WithCloudSearchContext.tsx +++ b/src/js/components/ManageMenu/CommunityCloud/WithCloudSearchContext.tsx @@ -164,6 +164,7 @@ const useRequestIds = () => { export interface CloudSearchContext { isErrored: boolean doSearch: (paramsDelta?: Partial) => void + isLoading: boolean isSearching: boolean searchParams: CloudSearchParams searchResults: CloudSearchResults | undefined @@ -175,6 +176,7 @@ const useSearchApi = () => { const { api } = useRestAPI() const { isCurrentRequest, nextRequestId } = useRequestIds() const [currentSearch, setCurrentSearch] = useState(false) + const [isLoading, setIsLoading] = useState(false) const [searchResults, setSearchResults] = useState() const [availableFilters, setAvailableFilters] = useState({}) @@ -182,6 +184,7 @@ const useSearchApi = () => { const requestId = nextRequestId() const isFeaturedSearch = '' === params.query.trim() + setIsLoading(true) setCurrentSearch(!isFeaturedSearch) const baseUrl = isFeaturedSearch ? SEARCH_URLS.FEATURED : SEARCH_URLS.SEARCH_QUERY @@ -197,10 +200,16 @@ const useSearchApi = () => { setSearchResults(false) } }) - .finally(() => setCurrentSearch(false)) + .finally(() => { + if (isCurrentRequest(requestId)) { + setCurrentSearch(false) + setIsLoading(false) + } + }) }, [api, nextRequestId, isCurrentRequest]) return { + isLoading, isSearching: currentSearch, searchResults, availableFilters, @@ -211,7 +220,7 @@ const useSearchApi = () => { const [Context, useCloudSearch] = createContextHook('useCloudSearch') export const WithCloudSearchContext: React.FC = ({ children }) => { - const { isSearching, makeSearchRequest, availableFilters, searchResults } = useSearchApi() + const { isLoading, isSearching, makeSearchRequest, availableFilters, searchResults } = useSearchApi() const [searchParams, setSearchParams] = useState(fetchSearchQueryParams) const [madeInitialRequest, setMadeInitialRequest] = useState(false) @@ -236,6 +245,7 @@ export const WithCloudSearchContext: React.FC = ({ children } const value: CloudSearchContext = { doSearch, + isLoading, isSearching, searchParams, availableFilters, diff --git a/src/js/components/common/LoadingStatusNotices.tsx b/src/js/components/common/LoadingStatusNotices.tsx new file mode 100644 index 000000000..4b4df5e54 --- /dev/null +++ b/src/js/components/common/LoadingStatusNotices.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { __ } from '@wordpress/i18n' +import type { ReactNode } from 'react' + +export interface LoadingStatusNoticesProps { + isLoading: boolean + errorMessage: string | undefined + errorPrefix: ReactNode + loadingNotice: ReactNode +} + +export const LoadingStatusNotices: React.FC = ({ + isLoading, + errorMessage, + errorPrefix, + loadingNotice +}) => { + switch (true) { + case errorMessage !== undefined: + return ( +
+

{errorPrefix} {errorMessage}

+
+ ) + + case isLoading: + return ( +
+

{loadingNotice}

+
+ ) + + default: + return ( +
+

{__('An unknown error occurred. Please try again.', 'code-snippets')}

+
+ ) + } +} diff --git a/tests/e2e/code-snippets-community-featured.spec.ts b/tests/e2e/code-snippets-community-featured.spec.ts index b6470004d..a570a0558 100644 --- a/tests/e2e/code-snippets-community-featured.spec.ts +++ b/tests/e2e/code-snippets-community-featured.spec.ts @@ -1,6 +1,39 @@ import { expect, test } from '@playwright/test' import { TIMEOUTS, URLS } from './helpers/constants' +const isFeaturedRequest = (url: URL): boolean => + url.pathname.includes('/cloud/snippets/featured') || + true === url.searchParams.get('rest_route')?.includes('/cloud/snippets/featured') + +const makeCloudSnippet = (id: number, name: string, localId: number | null = null) => ({ + id, + slug: `mock-cloud-snippet-${id}`, + name, + description: 'Mock description', + code: ' ({ + snippets, + page: 1, + total_pages: 1, + total_snippets: snippets.length, + available_filters: {} +}) + test.describe('Community Cloud Featured Snippets', () => { const jsErrors: string[] = [] @@ -67,41 +100,49 @@ test.describe('Community Cloud Featured Snippets', () => { await expect(page.locator('.cloud-snippets-heading', { hasText: 'Featured Snippets' })).not.toBeVisible() }) - test('Table checkboxes share the cloud selection state', async ({ page }) => { - await page.route( - url => - url.pathname.includes('/cloud/snippets/featured') || - true === url.searchParams.get('rest_route')?.includes('/cloud/snippets/featured'), - route => route.fulfill({ + test('Shows a page-level notice while featured snippets load', async ({ page }) => { + let releaseRequest = () => undefined + const requestPending = new Promise(resolve => { + releaseRequest = resolve + }) + + await page.route(isFeaturedRequest, async route => { + await requestPending + await route.fulfill({ contentType: 'application/json', - body: JSON.stringify({ - snippets: [{ - id: 501, - slug: 'mock-cloud-snippet', - name: 'Mock Cloud Snippet', - description: 'Mock description', - code: ' { + await page.route(isFeaturedRequest, route => route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ message: 'Cloud unavailable' }) + })) + await page.reload() + + await expect(page.locator('.cloud-search .notice-error')) + .toContainText('An error occurred while fetching search results. Please try again.') + }) + + test('Table checkboxes share the cloud selection state', async ({ page }) => { + await page.route(isFeaturedRequest, route => route.fulfill({ + contentType: 'application/json', + body: JSON.stringify(makeFeaturedResponse()) + })) await page.reload() const table = page.locator('.cloud-snippets-table') From 6a32210ba07b6043b500c4f4560f1315ba92d9ae Mon Sep 17 00:00:00 2001 From: Imants Date: Thu, 23 Jul 2026 19:34:42 +0300 Subject: [PATCH 02/14] feat: add cloud preview quick actions --- .../components/common/SnippetPreviewModal.tsx | 36 ++++++++++++++----- .../common/cloud/CloudSnippetPreviewModal.tsx | 2 ++ .../code-snippets-community-featured.spec.ts | 25 +++++++++++++ 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/js/components/common/SnippetPreviewModal.tsx b/src/js/components/common/SnippetPreviewModal.tsx index dc5b4c5e8..84ce02198 100644 --- a/src/js/components/common/SnippetPreviewModal.tsx +++ b/src/js/components/common/SnippetPreviewModal.tsx @@ -35,6 +35,7 @@ export interface SnippetPreviewModalProps { setIsOpen: (isOpen: boolean) => void snippet?: Snippet extraActions?: PreviewExtraActions + footerActions?: ReactNode } const EDITOR_MODES: Record = { @@ -238,6 +239,27 @@ const PreviewTypeBadge: React.FC<{ type: SnippetType }> = ({ type }) => +interface PreviewFooterProps { + snippet?: Snippet + extraActions?: PreviewExtraActions + footerActions?: ReactNode + closeModal: () => void +} + +const PreviewFooter: React.FC = ({ + snippet, + extraActions, + footerActions, + closeModal +}) => + snippet + ? + : footerActions + ?
+
{footerActions}
+
+ : null + /** * Modal for quickly viewing a snippet's code in a read-only CodeMirror editor, * without navigating to the edit page. Shared between local snippets and cloud @@ -251,7 +273,8 @@ export const SnippetPreviewModal: React.FC = ({ isOpen, setIsOpen, snippet, - extraActions + extraActions, + footerActions }) => { const textareaRef = useRef(null) @@ -291,13 +314,10 @@ export const SnippetPreviewModal: React.FC = ({ /> - {snippet - ? setIsOpen(false)} - /> - : null} + setIsOpen(false)} + /> : null } diff --git a/src/js/components/common/cloud/CloudSnippetPreviewModal.tsx b/src/js/components/common/cloud/CloudSnippetPreviewModal.tsx index 5e0c66511..e0fc8797a 100644 --- a/src/js/components/common/cloud/CloudSnippetPreviewModal.tsx +++ b/src/js/components/common/cloud/CloudSnippetPreviewModal.tsx @@ -1,6 +1,7 @@ import React from 'react' import { getSnippetType } from '../../../utils/snippets/snippets' import { SnippetPreviewModal } from '../SnippetPreviewModal' +import { CloudSnippetDownloadButton } from './CloudSnippetDownloadButton' import type { CloudSnippetSchema } from '../../../types/schema/CloudSnippetSchema' export interface CloudSnippetPreviewModalProps { @@ -16,4 +17,5 @@ export const CloudSnippetPreviewModal: React.FC = type={getSnippetType(snippet)} isOpen={isOpen} setIsOpen={setIsOpen} + footerActions={} /> diff --git a/tests/e2e/code-snippets-community-featured.spec.ts b/tests/e2e/code-snippets-community-featured.spec.ts index a570a0558..4601eacd8 100644 --- a/tests/e2e/code-snippets-community-featured.spec.ts +++ b/tests/e2e/code-snippets-community-featured.spec.ts @@ -138,6 +138,31 @@ test.describe('Community Cloud Featured Snippets', () => { .toContainText('An error occurred while fetching search results. Please try again.') }) + test('Preview offers the same download or edit action as its card', async ({ page }) => { + await page.route(isFeaturedRequest, route => route.fulfill({ + contentType: 'application/json', + body: JSON.stringify(makeFeaturedResponse([ + makeCloudSnippet(501, 'Downloadable Cloud Snippet'), + makeCloudSnippet(502, 'Installed Cloud Snippet', 42) + ])) + })) + await page.reload() + + const openPreview = async (snippetName: string) => { + await page.getByRole('button', { name: snippetName }).click() + await expect(page.locator('.code-snippets-preview-modal')).toBeVisible() + } + + await openPreview('Downloadable Cloud Snippet') + await expect(page.locator('.code-snippets-preview-modal').getByRole('button', { name: 'Download' })) + .toBeVisible() + await page.getByRole('button', { name: 'Close' }).click() + + await openPreview('Installed Cloud Snippet') + await expect(page.locator('.code-snippets-preview-modal').getByRole('link', { name: 'Edit' })) + .toBeVisible() + }) + test('Table checkboxes share the cloud selection state', async ({ page }) => { await page.route(isFeaturedRequest, route => route.fulfill({ contentType: 'application/json', From 4ad21184ad7d1caca6bbd34d673fb16ad4c3e4cd Mon Sep 17 00:00:00 2001 From: Imants Date: Thu, 23 Jul 2026 19:35:09 +0300 Subject: [PATCH 03/14] fix: align preview modal type badge --- src/css/common/_modal.scss | 3 ++- tests/e2e/code-snippets-preview.spec.ts | 30 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/css/common/_modal.scss b/src/css/common/_modal.scss index 3a90d67f8..c8bca6d05 100644 --- a/src/css/common/_modal.scss +++ b/src/css/common/_modal.scss @@ -71,8 +71,9 @@ // the header area, clear of the close button. .code-snippets-preview-modal__badge { position: absolute; - inset-block-start: 18px; + inset-block-start: 35px; inset-inline-end: 56px; + transform: translateY(-50%); } // Header and footer stay pinned; the CodeMirror editor is the only diff --git a/tests/e2e/code-snippets-preview.spec.ts b/tests/e2e/code-snippets-preview.spec.ts index eed05a74a..d88b15f12 100644 --- a/tests/e2e/code-snippets-preview.spec.ts +++ b/tests/e2e/code-snippets-preview.spec.ts @@ -5,6 +5,8 @@ import { wpCli } from './helpers/wpCli' import type { Locator, Page } from '@playwright/test' const MAXIMUM_FOCUS_ATTEMPTS = 10 +const MAXIMUM_BADGE_ALIGNMENT_OFFSET = 4 +const PREVIEW_VIEWPORT_WIDTHS = [1280, 640] // Disabling the admin's "Syntax Highlighting" preference makes // wp_enqueue_code_editor() a no-op, so window.wp.codeEditor is undefined when @@ -130,6 +132,34 @@ test.describe('Code Snippets Preview Modal', () => { await expect(editor.locator('[aria-label="Snippet code preview"]')).toBeAttached() }) + test('Preview type badge stays aligned with the title', async ({ page }) => { + await openPreviewEditor(page) + + const modal = page.locator('.code-snippets-preview-modal') + const title = modal.locator('.components-modal__header-heading') + const badge = modal.locator('.code-snippets-preview-modal__badge') + const closeButton = modal.locator('.components-modal__header').getByRole('button', { name: 'Close' }) + + for (const width of PREVIEW_VIEWPORT_WIDTHS) { + await page.setViewportSize({ width, height: 800 }) + const [titleBox, badgeBox, closeBox] = + await Promise.all([title.boundingBox(), badge.boundingBox(), closeButton.boundingBox()]) + + expect(titleBox).not.toBeNull() + expect(badgeBox).not.toBeNull() + expect(closeBox).not.toBeNull() + + if (titleBox && badgeBox && closeBox) { + const titleCenter = titleBox.y + titleBox.height / 2 + const badgeCenter = badgeBox.y + badgeBox.height / 2 + + expect(Math.abs(titleCenter - badgeCenter)).toBeLessThanOrEqual(MAXIMUM_BADGE_ALIGNMENT_OFFSET) + expect(titleBox.x + titleBox.width).toBeLessThan(badgeBox.x) + expect(badgeBox.x + badgeBox.width).toBeLessThanOrEqual(closeBox.x) + } + } + }) + for (const keypress of ['Tab', 'Shift+Tab']) { test(`${keypress} leaves the preview editor`, async ({ page }) => { const editor = await openPreviewEditor(page) From 651de0d0d9c7ebabd893a185ef9c3004e40207bb Mon Sep 17 00:00:00 2001 From: Imants Date: Thu, 23 Jul 2026 19:43:54 +0300 Subject: [PATCH 04/14] fix: prioritize loading state over stale search errors --- .../ManageMenu/CommunityCloud/CloudSearch.tsx | 5 +++-- src/js/components/common/LoadingStatusNotices.tsx | 14 ++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx b/src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx index 2a10f1359..b3dbab637 100644 --- a/src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx +++ b/src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx @@ -225,8 +225,9 @@ const SearchStatus = () => { return isErrored || isLoading ? : null diff --git a/src/js/components/common/LoadingStatusNotices.tsx b/src/js/components/common/LoadingStatusNotices.tsx index 4b4df5e54..256c60933 100644 --- a/src/js/components/common/LoadingStatusNotices.tsx +++ b/src/js/components/common/LoadingStatusNotices.tsx @@ -5,28 +5,26 @@ import type { ReactNode } from 'react' export interface LoadingStatusNoticesProps { isLoading: boolean errorMessage: string | undefined - errorPrefix: ReactNode loadingNotice: ReactNode } export const LoadingStatusNotices: React.FC = ({ isLoading, errorMessage, - errorPrefix, loadingNotice }) => { switch (true) { - case errorMessage !== undefined: + case isLoading: return ( -
-

{errorPrefix} {errorMessage}

+
+

{loadingNotice}

) - case isLoading: + case errorMessage !== undefined: return ( -
-

{loadingNotice}

+
+

{errorMessage}

) From f78f3bf9d4c52cbf89e9cb51ccf8820298af1d12 Mon Sep 17 00:00:00 2001 From: Imants Date: Thu, 23 Jul 2026 22:13:10 +0300 Subject: [PATCH 05/14] fix: label cloud status notices --- .../ManageMenu/CommunityCloud/CloudSearch.tsx | 1 + src/js/components/common/LoadingStatusNotices.tsx | 10 ++++++---- tests/e2e/code-snippets-community-featured.spec.ts | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx b/src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx index b3dbab637..88d8ac701 100644 --- a/src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx +++ b/src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx @@ -229,6 +229,7 @@ const SearchStatus = () => { ? __('An error occurred while fetching search results. Please try again.', 'code-snippets') : undefined} loadingNotice={__('Loading community snippets…', 'code-snippets')} + noticeLabel={__('Community snippets status', 'code-snippets')} /> : null } diff --git a/src/js/components/common/LoadingStatusNotices.tsx b/src/js/components/common/LoadingStatusNotices.tsx index 256c60933..5241fa7ef 100644 --- a/src/js/components/common/LoadingStatusNotices.tsx +++ b/src/js/components/common/LoadingStatusNotices.tsx @@ -6,31 +6,33 @@ export interface LoadingStatusNoticesProps { isLoading: boolean errorMessage: string | undefined loadingNotice: ReactNode + noticeLabel: string } export const LoadingStatusNotices: React.FC = ({ isLoading, errorMessage, - loadingNotice + loadingNotice, + noticeLabel }) => { switch (true) { case isLoading: return ( -
+

{loadingNotice}

) case errorMessage !== undefined: return ( -
+

{errorMessage}

) default: return ( -
+

{__('An unknown error occurred. Please try again.', 'code-snippets')}

) diff --git a/tests/e2e/code-snippets-community-featured.spec.ts b/tests/e2e/code-snippets-community-featured.spec.ts index 4601eacd8..57df167e9 100644 --- a/tests/e2e/code-snippets-community-featured.spec.ts +++ b/tests/e2e/code-snippets-community-featured.spec.ts @@ -117,7 +117,7 @@ test.describe('Community Cloud Featured Snippets', () => { await page.reload() try { - await expect(page.locator('.cloud-search .notice')) + await expect(page.getByRole('region', { name: 'Community snippets status' })) .toContainText('Loading community snippets…') } finally { releaseRequest() From bbd20a34d68d7f31471253bc20268ad148de5ec2 Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 24 Jul 2026 15:43:00 +0300 Subject: [PATCH 06/14] fix: announce cloud request status --- src/js/components/common/LoadingStatusNotices.tsx | 13 +++++++------ tests/e2e/code-snippets-community-featured.spec.ts | 8 ++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/js/components/common/LoadingStatusNotices.tsx b/src/js/components/common/LoadingStatusNotices.tsx index 5241fa7ef..c4de79571 100644 --- a/src/js/components/common/LoadingStatusNotices.tsx +++ b/src/js/components/common/LoadingStatusNotices.tsx @@ -1,5 +1,6 @@ import React from 'react' import { __ } from '@wordpress/i18n' +import { Notice } from './Notice' import type { ReactNode } from 'react' export interface LoadingStatusNoticesProps { @@ -18,23 +19,23 @@ export const LoadingStatusNotices: React.FC = ({ switch (true) { case isLoading: return ( -
+

{loadingNotice}

-
+ ) case errorMessage !== undefined: return ( -
+

{errorMessage}

-
+ ) default: return ( -
+

{__('An unknown error occurred. Please try again.', 'code-snippets')}

-
+ ) } } diff --git a/tests/e2e/code-snippets-community-featured.spec.ts b/tests/e2e/code-snippets-community-featured.spec.ts index 57df167e9..5ab15f3b9 100644 --- a/tests/e2e/code-snippets-community-featured.spec.ts +++ b/tests/e2e/code-snippets-community-featured.spec.ts @@ -101,9 +101,9 @@ test.describe('Community Cloud Featured Snippets', () => { }) test('Shows a page-level notice while featured snippets load', async ({ page }) => { - let releaseRequest = () => undefined + let releaseRequest: () => void = () => undefined const requestPending = new Promise(resolve => { - releaseRequest = resolve + releaseRequest = () => resolve() }) await page.route(isFeaturedRequest, async route => { @@ -117,7 +117,7 @@ test.describe('Community Cloud Featured Snippets', () => { await page.reload() try { - await expect(page.getByRole('region', { name: 'Community snippets status' })) + await expect(page.getByRole('status', { name: 'Community snippets status' })) .toContainText('Loading community snippets…') } finally { releaseRequest() @@ -134,7 +134,7 @@ test.describe('Community Cloud Featured Snippets', () => { })) await page.reload() - await expect(page.locator('.cloud-search .notice-error')) + await expect(page.getByRole('alert', { name: 'Community snippets status' })) .toContainText('An error occurred while fetching search results. Please try again.') }) From 2f9cb8b4cc195bc814216da619b1bbc9cf59af09 Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 24 Jul 2026 15:47:05 +0300 Subject: [PATCH 07/14] fix: refresh cloud actions after download --- .../CommunityCloud/CloudSnippetsTable.tsx | 43 +++++++++++++++---- .../CommunityCloud/SearchResult.tsx | 5 ++- .../cloud/CloudSnippetDownloadButton.tsx | 9 ++-- .../common/cloud/CloudSnippetPreviewModal.tsx | 10 ++++- .../code-snippets-community-featured.spec.ts | 35 +++++++++++++++ 5 files changed, 87 insertions(+), 15 deletions(-) diff --git a/src/js/components/ManageMenu/CommunityCloud/CloudSnippetsTable.tsx b/src/js/components/ManageMenu/CommunityCloud/CloudSnippetsTable.tsx index 1be29a711..e18182771 100644 --- a/src/js/components/ManageMenu/CommunityCloud/CloudSnippetsTable.tsx +++ b/src/js/components/ManageMenu/CommunityCloud/CloudSnippetsTable.tsx @@ -7,6 +7,7 @@ import { Button } from '../../common/Button' import { CloudSnippetDownloadButton } from '../../common/cloud/CloudSnippetDownloadButton' import { CloudSnippetPreviewModal } from '../../common/cloud/CloudSnippetPreviewModal' import { CloudStatusBadge } from '../../common/cloud/CloudStatusBadge' +import { useCloudSearch } from './WithCloudSearchContext' import type { CloudSnippetSchema } from '../../../types/schema/CloudSnippetSchema' import type { Dispatch, SetStateAction } from 'react' @@ -27,6 +28,38 @@ interface CloudSnippetRowProps { setSelected: SetSelected } +interface CloudSnippetActionsProps extends Pick { + isPreviewOpen: boolean + setIsPreviewOpen: (isOpen: boolean) => void +} + +const CloudSnippetActions: React.FC = ({ + snippet, + isPreviewOpen, + setIsPreviewOpen +}) => { + const { doSearch } = useCloudSearch() + + return ( + <> +
+ + + +
+ + + + ) +} + const CloudSnippetRow: React.FC = ({ snippet, selected, setSelected }) => { const [isPreviewOpen, setIsPreviewOpen] = useState(false) @@ -69,15 +102,7 @@ const CloudSnippetRow: React.FC = ({ snippet, selected, se -
- - - -
- - + ) diff --git a/src/js/components/ManageMenu/CommunityCloud/SearchResult.tsx b/src/js/components/ManageMenu/CommunityCloud/SearchResult.tsx index e59313549..58cf6bfed 100644 --- a/src/js/components/ManageMenu/CommunityCloud/SearchResult.tsx +++ b/src/js/components/ManageMenu/CommunityCloud/SearchResult.tsx @@ -10,6 +10,7 @@ import { CloudSnippetPreviewModal } from '../../common/cloud/CloudSnippetPreview import { CloudStatusIndicator } from '../../common/cloud/CloudStatusBadge' import { CloudUpdateIcon } from '../../common/icons/CloudIcons' import { SnippetCard } from '../../common/SnippetCard' +import { useCloudSearch } from './WithCloudSearchContext' import type { ReactNode } from 'react' import type { CloudSnippetSchema } from '../../../types/schema/CloudSnippetSchema' @@ -100,6 +101,7 @@ export const SearchResult: React.FC = ({ isSelected = false, onSelectedChange }) => { + const { doSearch } = useCloudSearch() const [isPreviewOpen, setIsPreviewOpen] = useState(false) return ( @@ -126,7 +128,7 @@ export const SearchResult: React.FC = ({ {__('Preview', 'code-snippets')} - + } > @@ -135,6 +137,7 @@ export const SearchResult: React.FC = ({ snippet={snippet} isOpen={isPreviewOpen} setIsOpen={setIsPreviewOpen} + onDownloaded={doSearch} /> ) diff --git a/src/js/components/common/cloud/CloudSnippetDownloadButton.tsx b/src/js/components/common/cloud/CloudSnippetDownloadButton.tsx index d3e9110fd..91c676e97 100644 --- a/src/js/components/common/cloud/CloudSnippetDownloadButton.tsx +++ b/src/js/components/common/cloud/CloudSnippetDownloadButton.tsx @@ -17,15 +17,17 @@ export interface DownloadSnippetResponse { } interface CloudSnippetDownloadButtonProps { + onDownloaded: VoidFunction snippet: CloudSnippetSchema } -export const CloudSnippetDownloadButton: React.FC = ({ snippet }) => { +export const CloudSnippetDownloadButton: React.FC = ({ snippet, onDownloaded }) => { const { api } = useRestAPI() const [isWorking, setIsWorking] = useState(false) const [errorMessage, setErrorMessage] = useState() - const [localSnippetId, setLocalSnippetId] = useState(snippet.local_id ?? 0) + const [downloadedSnippetId, setDownloadedSnippetId] = useState(0) const [isUpsellOpen, setIsUpsellOpen] = useState(false) + const localSnippetId = snippet.local_id ?? downloadedSnippetId if (localSnippetId) { return ( @@ -52,7 +54,8 @@ export const CloudSnippetDownloadButton: React.FC(`${REST_BASES.cloud.snippets}/${snippet.id}/download`) .then(response => { - setLocalSnippetId(response.snippet_id) + setDownloadedSnippetId(response.snippet_id) + onDownloaded() }) .catch((error: unknown) => { setErrorMessage('string' === typeof error diff --git a/src/js/components/common/cloud/CloudSnippetPreviewModal.tsx b/src/js/components/common/cloud/CloudSnippetPreviewModal.tsx index e0fc8797a..946b8263b 100644 --- a/src/js/components/common/cloud/CloudSnippetPreviewModal.tsx +++ b/src/js/components/common/cloud/CloudSnippetPreviewModal.tsx @@ -6,16 +6,22 @@ import type { CloudSnippetSchema } from '../../../types/schema/CloudSnippetSchem export interface CloudSnippetPreviewModalProps { isOpen: boolean + onDownloaded: VoidFunction snippet: CloudSnippetSchema setIsOpen: (isOpen: boolean) => void } -export const CloudSnippetPreviewModal: React.FC = ({ snippet, isOpen, setIsOpen }) => +export const CloudSnippetPreviewModal: React.FC = ({ + snippet, + isOpen, + setIsOpen, + onDownloaded +}) => } + footerActions={} /> diff --git a/tests/e2e/code-snippets-community-featured.spec.ts b/tests/e2e/code-snippets-community-featured.spec.ts index 5ab15f3b9..90851665e 100644 --- a/tests/e2e/code-snippets-community-featured.spec.ts +++ b/tests/e2e/code-snippets-community-featured.spec.ts @@ -5,6 +5,10 @@ const isFeaturedRequest = (url: URL): boolean => url.pathname.includes('/cloud/snippets/featured') || true === url.searchParams.get('rest_route')?.includes('/cloud/snippets/featured') +const isSnippetDownloadRequest = (url: URL): boolean => + url.pathname.includes('/cloud/snippets/501/download') || + true === url.searchParams.get('rest_route')?.includes('/cloud/snippets/501/download') + const makeCloudSnippet = (id: number, name: string, localId: number | null = null) => ({ id, slug: `mock-cloud-snippet-${id}`, @@ -163,6 +167,37 @@ test.describe('Community Cloud Featured Snippets', () => { .toBeVisible() }) + test('Downloading from preview refreshes every snippet action', async ({ page }) => { + let isDownloaded = false + let downloadRequests = 0 + + await page.route(isFeaturedRequest, route => route.fulfill({ + contentType: 'application/json', + body: JSON.stringify(makeFeaturedResponse([ + makeCloudSnippet(501, 'Downloadable Cloud Snippet', isDownloaded ? 42 : null) + ])) + })) + await page.route(isSnippetDownloadRequest, route => { + isDownloaded = true + downloadRequests += 1 + return route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ success: true, snippet_id: 42, link_id: 501 }) + }) + }) + await page.reload() + + const card = page.locator('.cloud-search-result', { hasText: 'Downloadable Cloud Snippet' }) + await card.getByRole('button', { name: 'Downloadable Cloud Snippet' }).click() + + const preview = page.locator('.code-snippets-preview-modal') + await preview.getByRole('button', { name: 'Download' }).click() + + await expect(card.getByRole('link', { name: 'Edit' })).toBeVisible() + await expect(page.getByRole('button', { name: 'Download' })).toHaveCount(0) + expect(downloadRequests).toBe(1) + }) + test('Table checkboxes share the cloud selection state', async ({ page }) => { await page.route(isFeaturedRequest, route => route.fulfill({ contentType: 'application/json', From f740d76503f8761841ee24bbceb4304cb2b1d879 Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 24 Jul 2026 15:50:25 +0300 Subject: [PATCH 08/14] fix: place preview badge in content flow --- src/css/common/_modal.scss | 16 ++++------ .../components/common/SnippetPreviewModal.tsx | 4 --- tests/e2e/code-snippets-preview.spec.ts | 30 +++++++++---------- 3 files changed, 20 insertions(+), 30 deletions(-) diff --git a/src/css/common/_modal.scss b/src/css/common/_modal.scss index c8bca6d05..8eadf5d28 100644 --- a/src/css/common/_modal.scss +++ b/src/css/common/_modal.scss @@ -66,14 +66,10 @@ border-block-end-color: #ddd; } - // The badge lives in the modal content because the minimum-supported - // WordPress Modal (5.5–6.3) has no headerActions prop; shift it up into - // the header area, clear of the close button. .code-snippets-preview-modal__badge { - position: absolute; - inset-block-start: 35px; - inset-inline-end: 56px; - transform: translateY(-50%); + display: flex; + flex-shrink: 0; + justify-content: flex-end; } // Header and footer stay pinned; the CodeMirror editor is the only @@ -86,9 +82,9 @@ overflow: hidden; } - // The modal component wraps its children in an unstyled focus container; - // it must join the flex chain or the editor grows to its content height. - .components-modal__header + div { + // Newer Modal versions wrap children in an unstyled focus container; older + // supported versions render the badge directly after the header. + .components-modal__header + div:not(.code-snippets-preview-modal__badge) { display: flex; flex-flow: column; flex: 1; diff --git a/src/js/components/common/SnippetPreviewModal.tsx b/src/js/components/common/SnippetPreviewModal.tsx index 84ce02198..47bfc9c6f 100644 --- a/src/js/components/common/SnippetPreviewModal.tsx +++ b/src/js/components/common/SnippetPreviewModal.tsx @@ -230,10 +230,6 @@ const SnippetPreviewActions: React.FC = ({ ) } -/** - * The minimum-supported WordPress Modal (5.5–6.3) has no headerActions prop, - * so the badge renders in the content and CSS moves it into the header area. - */ const PreviewTypeBadge: React.FC<{ type: SnippetType }> = ({ type }) =>
diff --git a/tests/e2e/code-snippets-preview.spec.ts b/tests/e2e/code-snippets-preview.spec.ts index d88b15f12..c30bfb37b 100644 --- a/tests/e2e/code-snippets-preview.spec.ts +++ b/tests/e2e/code-snippets-preview.spec.ts @@ -107,8 +107,6 @@ test.describe('Code Snippets Preview Modal', () => { await expect(codeArea).toBeVisible({ timeout: TIMEOUTS.DEFAULT }) await expect(codeArea).toHaveValue(new RegExp(snippetName)) - // The type badge renders in the modal content (the minimum-supported - // WordPress Modal has no headerActions prop). await expect(preview.locator('.code-snippets-preview-modal__badge .badge')) .toBeVisible() @@ -132,30 +130,30 @@ test.describe('Code Snippets Preview Modal', () => { await expect(editor.locator('[aria-label="Snippet code preview"]')).toBeAttached() }) - test('Preview type badge stays aligned with the title', async ({ page }) => { + test('Preview type badge sits above code at inline end', async ({ page }) => { await openPreviewEditor(page) const modal = page.locator('.code-snippets-preview-modal') - const title = modal.locator('.components-modal__header-heading') - const badge = modal.locator('.code-snippets-preview-modal__badge') - const closeButton = modal.locator('.components-modal__header').getByRole('button', { name: 'Close' }) + const badge = modal.locator('.code-snippets-preview-modal__badge .badge') + const editor = modal.locator('.code-snippets-preview-modal__editor') + + await expect(badge).toBeVisible() for (const width of PREVIEW_VIEWPORT_WIDTHS) { await page.setViewportSize({ width, height: 800 }) - const [titleBox, badgeBox, closeBox] = - await Promise.all([title.boundingBox(), badge.boundingBox(), closeButton.boundingBox()]) + const [badgeBox, editorBox] = await Promise.all([badge.boundingBox(), editor.boundingBox()]) - expect(titleBox).not.toBeNull() expect(badgeBox).not.toBeNull() - expect(closeBox).not.toBeNull() + expect(editorBox).not.toBeNull() - if (titleBox && badgeBox && closeBox) { - const titleCenter = titleBox.y + titleBox.height / 2 - const badgeCenter = badgeBox.y + badgeBox.height / 2 + if (badgeBox && editorBox) { + const badgeInlineEnd = badgeBox.x + badgeBox.width + const editorInlineEnd = editorBox.x + editorBox.width - expect(Math.abs(titleCenter - badgeCenter)).toBeLessThanOrEqual(MAXIMUM_BADGE_ALIGNMENT_OFFSET) - expect(titleBox.x + titleBox.width).toBeLessThan(badgeBox.x) - expect(badgeBox.x + badgeBox.width).toBeLessThanOrEqual(closeBox.x) + expect(Math.abs(badgeInlineEnd - editorInlineEnd)).toBeLessThanOrEqual( + MAXIMUM_BADGE_ALIGNMENT_OFFSET + ) + expect(badgeBox.y + badgeBox.height).toBeLessThanOrEqual(editorBox.y) } } }) From 0397e17cf536a20c743438607a9b4fc4ac9ec9e9 Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 24 Jul 2026 15:52:26 +0300 Subject: [PATCH 09/14] fix: establish cloud card view in e2e --- .../code-snippets-community-featured.spec.ts | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/tests/e2e/code-snippets-community-featured.spec.ts b/tests/e2e/code-snippets-community-featured.spec.ts index 90851665e..802d8ddb9 100644 --- a/tests/e2e/code-snippets-community-featured.spec.ts +++ b/tests/e2e/code-snippets-community-featured.spec.ts @@ -1,5 +1,17 @@ import { expect, test } from '@playwright/test' import { TIMEOUTS, URLS } from './helpers/constants' +import type { Page } from '@playwright/test' + +const switchSnippetView = async (page: Page, view: 'Card view' | 'Table view') => { + const saved = page + .waitForResponse( + response => response.url().includes('/snippet-view') && 'GET' !== response.request().method(), + { timeout: TIMEOUTS.SHORT } + ) + .catch(() => undefined) + await page.getByRole('button', { name: view }).click() + await saved +} const isFeaturedRequest = (url: URL): boolean => url.pathname.includes('/cloud/snippets/featured') || @@ -187,15 +199,22 @@ test.describe('Community Cloud Featured Snippets', () => { }) await page.reload() - const card = page.locator('.cloud-search-result', { hasText: 'Downloadable Cloud Snippet' }) - await card.getByRole('button', { name: 'Downloadable Cloud Snippet' }).click() + await switchSnippetView(page, 'Card view') - const preview = page.locator('.code-snippets-preview-modal') - await preview.getByRole('button', { name: 'Download' }).click() + try { + const card = page.locator('.cloud-search-result', { hasText: 'Downloadable Cloud Snippet' }) + await card.getByRole('button', { name: 'Downloadable Cloud Snippet' }).click() + + const preview = page.locator('.code-snippets-preview-modal') + await preview.getByRole('button', { name: 'Download' }).click() - await expect(card.getByRole('link', { name: 'Edit' })).toBeVisible() - await expect(page.getByRole('button', { name: 'Download' })).toHaveCount(0) - expect(downloadRequests).toBe(1) + await expect(card.getByRole('link', { name: 'Edit' })).toBeVisible() + await expect(page.getByRole('button', { name: 'Download' })).toHaveCount(0) + expect(downloadRequests).toBe(1) + } finally { + await page.getByRole('button', { name: 'Close' }).click().catch(() => undefined) + await switchSnippetView(page, 'Table view').catch(() => undefined) + } }) test('Table checkboxes share the cloud selection state', async ({ page }) => { From 04ac27261b9a56efaa9938b702adb636abc7f73c Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 24 Jul 2026 15:54:08 +0300 Subject: [PATCH 10/14] fix: export cloud download button props --- src/js/components/common/cloud/CloudSnippetDownloadButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/components/common/cloud/CloudSnippetDownloadButton.tsx b/src/js/components/common/cloud/CloudSnippetDownloadButton.tsx index 91c676e97..95b7ecbfb 100644 --- a/src/js/components/common/cloud/CloudSnippetDownloadButton.tsx +++ b/src/js/components/common/cloud/CloudSnippetDownloadButton.tsx @@ -16,7 +16,7 @@ export interface DownloadSnippetResponse { link_id: number } -interface CloudSnippetDownloadButtonProps { +export interface CloudSnippetDownloadButtonProps { onDownloaded: VoidFunction snippet: CloudSnippetSchema } From f71d56e1c430235ea39c76754691928ae1c2169c Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 24 Jul 2026 16:06:42 +0300 Subject: [PATCH 11/14] fix: stabilize preview badge test --- tests/e2e/code-snippets-preview.spec.ts | 35 ++++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/tests/e2e/code-snippets-preview.spec.ts b/tests/e2e/code-snippets-preview.spec.ts index c30bfb37b..6ded7360e 100644 --- a/tests/e2e/code-snippets-preview.spec.ts +++ b/tests/e2e/code-snippets-preview.spec.ts @@ -137,24 +137,27 @@ test.describe('Code Snippets Preview Modal', () => { const badge = modal.locator('.code-snippets-preview-modal__badge .badge') const editor = modal.locator('.code-snippets-preview-modal__editor') - await expect(badge).toBeVisible() - for (const width of PREVIEW_VIEWPORT_WIDTHS) { await page.setViewportSize({ width, height: 800 }) - const [badgeBox, editorBox] = await Promise.all([badge.boundingBox(), editor.boundingBox()]) - - expect(badgeBox).not.toBeNull() - expect(editorBox).not.toBeNull() - - if (badgeBox && editorBox) { - const badgeInlineEnd = badgeBox.x + badgeBox.width - const editorInlineEnd = editorBox.x + editorBox.width - - expect(Math.abs(badgeInlineEnd - editorInlineEnd)).toBeLessThanOrEqual( - MAXIMUM_BADGE_ALIGNMENT_OFFSET - ) - expect(badgeBox.y + badgeBox.height).toBeLessThanOrEqual(editorBox.y) - } + await expect(badge).toBeVisible() + await expect(editor).toBeVisible() + await expect(async () => { + const [badgeBox, editorBox] = + await Promise.all([badge.boundingBox(), editor.boundingBox()]) + + expect(badgeBox).not.toBeNull() + expect(editorBox).not.toBeNull() + + if (badgeBox && editorBox) { + const badgeInlineEnd = badgeBox.x + badgeBox.width + const editorInlineEnd = editorBox.x + editorBox.width + + expect(Math.abs(badgeInlineEnd - editorInlineEnd)).toBeLessThanOrEqual( + MAXIMUM_BADGE_ALIGNMENT_OFFSET + ) + expect(badgeBox.y + badgeBox.height).toBeLessThanOrEqual(editorBox.y) + } + }).toPass({ timeout: TIMEOUTS.SHORT }) } }) From cba70d5dc8966f1e6d386edeabea8fdc976385e4 Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 24 Jul 2026 17:25:20 +0300 Subject: [PATCH 12/14] fix: share cloud download state --- .../CommunityCloud/CommunityCloud.tsx | 9 +- .../cloud/CloudSnippetDownloadButton.tsx | 17 +-- .../WithCloudSnippetDownloadsContext.tsx | 44 ++++++++ .../code-snippets-community-featured.spec.ts | 100 ++++++++++++++++-- 4 files changed, 149 insertions(+), 21 deletions(-) create mode 100644 src/js/components/common/cloud/WithCloudSnippetDownloadsContext.tsx diff --git a/src/js/components/ManageMenu/CommunityCloud/CommunityCloud.tsx b/src/js/components/ManageMenu/CommunityCloud/CommunityCloud.tsx index 190713ac1..df21ffb5c 100644 --- a/src/js/components/ManageMenu/CommunityCloud/CommunityCloud.tsx +++ b/src/js/components/ManageMenu/CommunityCloud/CommunityCloud.tsx @@ -5,6 +5,7 @@ import { useSnippetView } from '../../../hooks/useSnippetView' import { fetchConstQueryParam, updateQueryParams } from '../../../utils/urls' import { ScreenMetaSlot } from '../../common/ScreenMetaSlot' import { SubnavTabs } from '../../common/SubnavTabs' +import { WithCloudSnippetDownloadsContext } from '../../common/cloud/WithCloudSnippetDownloadsContext' import { WithCloudSearchContext, useCloudSearch } from './WithCloudSearchContext' import { CloudSearch } from './CloudSearch' @@ -75,7 +76,9 @@ const CommunityCloudInner = () => { export const CommunityCloud = () => - - - + + + + + diff --git a/src/js/components/common/cloud/CloudSnippetDownloadButton.tsx b/src/js/components/common/cloud/CloudSnippetDownloadButton.tsx index 95b7ecbfb..fdbd9ba6a 100644 --- a/src/js/components/common/cloud/CloudSnippetDownloadButton.tsx +++ b/src/js/components/common/cloud/CloudSnippetDownloadButton.tsx @@ -8,6 +8,7 @@ import { isLicensed } from '../../../utils/screen' import { Button } from '../Button' import { ErrorTooltip } from '../Tooltip' import { UpsellDialog } from '../UpsellDialog' +import { useCloudSnippetDownloads } from './WithCloudSnippetDownloadsContext' import type { CloudSnippetSchema } from '../../../types/schema/CloudSnippetSchema' export interface DownloadSnippetResponse { @@ -23,11 +24,11 @@ export interface CloudSnippetDownloadButtonProps { export const CloudSnippetDownloadButton: React.FC = ({ snippet, onDownloaded }) => { const { api } = useRestAPI() - const [isWorking, setIsWorking] = useState(false) + const { downloadRecords, updateDownloadRecord } = useCloudSnippetDownloads() const [errorMessage, setErrorMessage] = useState() - const [downloadedSnippetId, setDownloadedSnippetId] = useState(0) const [isUpsellOpen, setIsUpsellOpen] = useState(false) - const localSnippetId = snippet.local_id ?? downloadedSnippetId + const { isDownloading = false, localId } = downloadRecords[snippet.id] ?? {} + const localSnippetId = snippet.local_id ?? localId if (localSnippetId) { return ( @@ -49,12 +50,12 @@ export const CloudSnippetDownloadButton: React.FC { - setIsWorking(true) + updateDownloadRecord(snippet.id, { isDownloading: true }) setErrorMessage(undefined) api.post(`${REST_BASES.cloud.snippets}/${snippet.id}/download`) .then(response => { - setDownloadedSnippetId(response.snippet_id) + updateDownloadRecord(snippet.id, { localId: response.snippet_id }) onDownloaded() }) .catch((error: unknown) => { @@ -62,15 +63,15 @@ export const CloudSnippetDownloadButton: React.FC setIsWorking(false)) + .finally(() => updateDownloadRecord(snippet.id, { isDownloading: false })) } return ( <> - {isWorking && } + {isDownloading && } {errorMessage && } - diff --git a/src/js/components/common/cloud/WithCloudSnippetDownloadsContext.tsx b/src/js/components/common/cloud/WithCloudSnippetDownloadsContext.tsx new file mode 100644 index 000000000..11dcb8cec --- /dev/null +++ b/src/js/components/common/cloud/WithCloudSnippetDownloadsContext.tsx @@ -0,0 +1,44 @@ +import React, { useCallback, useState } from 'react' +import { createContextHook } from '../../../utils/bootstrap' +import type { PropsWithChildren } from 'react' +import type { CloudSnippetSchema } from '../../../types/schema/CloudSnippetSchema' + +export interface CloudSnippetDownloadRecord { + isDownloading: boolean + localId?: number +} + +interface CloudSnippetDownloadsContext { + downloadRecords: Partial> + updateDownloadRecord: ( + snippetId: CloudSnippetSchema['id'], + update: Partial + ) => void +} + +const [Context, useCloudSnippetDownloads] = + createContextHook('useCloudSnippetDownloads') + +export const WithCloudSnippetDownloadsContext: React.FC = ({ children }) => { + const [downloadRecords, setDownloadRecords] = + useState({}) + const updateDownloadRecord = useCallback( + (snippetId, update) => setDownloadRecords(previous => ({ + ...previous, + [snippetId]: { + isDownloading: false, + ...previous[snippetId], + ...update + } + })), + [] + ) + + return ( + + {children} + + ) +} + +export { useCloudSnippetDownloads } diff --git a/tests/e2e/code-snippets-community-featured.spec.ts b/tests/e2e/code-snippets-community-featured.spec.ts index 802d8ddb9..9f0538294 100644 --- a/tests/e2e/code-snippets-community-featured.spec.ts +++ b/tests/e2e/code-snippets-community-featured.spec.ts @@ -179,18 +179,91 @@ test.describe('Community Cloud Featured Snippets', () => { .toBeVisible() }) - test('Downloading from preview refreshes every snippet action', async ({ page }) => { - let isDownloaded = false + test('Shares pending and downloaded state before refresh completes', async ({ page }) => { + let releaseDownload: () => void = () => undefined + let releaseRefresh: () => void = () => undefined + const downloadPending = new Promise(resolve => { + releaseDownload = () => resolve() + }) + const refreshPending = new Promise(resolve => { + releaseRefresh = () => resolve() + }) let downloadRequests = 0 + let featuredRequests = 0 - await page.route(isFeaturedRequest, route => route.fulfill({ - contentType: 'application/json', - body: JSON.stringify(makeFeaturedResponse([ - makeCloudSnippet(501, 'Downloadable Cloud Snippet', isDownloaded ? 42 : null) - ])) - })) + await page.route(isFeaturedRequest, async route => { + featuredRequests += 1 + + if (1 < featuredRequests) { + await refreshPending + } + + return route.fulfill({ + contentType: 'application/json', + body: JSON.stringify(makeFeaturedResponse([ + makeCloudSnippet(501, 'Downloadable Cloud Snippet') + ])) + }) + }) + await page.route(isSnippetDownloadRequest, async route => { + downloadRequests += 1 + await downloadPending + return route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ success: true, snippet_id: 42, link_id: 501 }) + }) + }) + await page.reload() + + await switchSnippetView(page, 'Card view') + + try { + const card = page.locator('.cloud-search-result', { hasText: 'Downloadable Cloud Snippet' }) + await card.getByRole('button', { name: 'Downloadable Cloud Snippet' }).click() + + const preview = page.locator('.code-snippets-preview-modal') + await preview.getByRole('button', { name: 'Download' }).click() + + await expect(preview.getByRole('button', { name: 'Download' })).toBeDisabled() + await expect(card.getByRole('button', { name: 'Download' })).toBeDisabled() + + releaseDownload() + await expect(preview.getByRole('link', { name: 'Edit' })).toBeVisible() + await expect.poll(() => featuredRequests).toBe(2) + await page.getByRole('button', { name: 'Close' }).click() + + await expect(card.getByRole('link', { name: 'Edit' })).toBeVisible() + await expect(card.getByRole('button', { name: 'Download' })).toHaveCount(0) + expect(downloadRequests).toBe(1) + } finally { + releaseDownload() + releaseRefresh() + await page.getByRole('button', { name: 'Close' }).click().catch(() => undefined) + await switchSnippetView(page, 'Table view').catch(() => undefined) + } + }) + + test('Keeps downloaded state when refresh fails', async ({ page }) => { + let downloadRequests = 0 + let featuredRequests = 0 + + await page.route(isFeaturedRequest, route => { + featuredRequests += 1 + + return 2 === featuredRequests + ? route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ message: 'Cloud unavailable' }) + }) + : route.fulfill({ + contentType: 'application/json', + body: JSON.stringify(makeFeaturedResponse([ + makeCloudSnippet(501, 'Downloadable Cloud Snippet') + ])) + }) + }) await page.route(isSnippetDownloadRequest, route => { - isDownloaded = true downloadRequests += 1 return route.fulfill({ contentType: 'application/json', @@ -208,8 +281,15 @@ test.describe('Community Cloud Featured Snippets', () => { const preview = page.locator('.code-snippets-preview-modal') await preview.getByRole('button', { name: 'Download' }).click() + await expect(page.getByRole('alert', { name: 'Community snippets status' })) + .toContainText('An error occurred while fetching search results. Please try again.') + + await page.locator('.cloud-search-form') + .getByRole('button', { name: 'Search Cloud Library' }) + .click() + await expect(card.getByRole('link', { name: 'Edit' })).toBeVisible() - await expect(page.getByRole('button', { name: 'Download' })).toHaveCount(0) + await expect(card.getByRole('button', { name: 'Download' })).toHaveCount(0) expect(downloadRequests).toBe(1) } finally { await page.getByRole('button', { name: 'Close' }).click().catch(() => undefined) From d8fe0f8cc43538361d0f4b8da2b27e6f4bb4327a Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 24 Jul 2026 17:45:05 +0300 Subject: [PATCH 13/14] fix: isolate cloud download e2e state --- .../e2e/code-snippets-community-featured.spec.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/e2e/code-snippets-community-featured.spec.ts b/tests/e2e/code-snippets-community-featured.spec.ts index 9f0538294..f066bb4d3 100644 --- a/tests/e2e/code-snippets-community-featured.spec.ts +++ b/tests/e2e/code-snippets-community-featured.spec.ts @@ -13,6 +13,14 @@ const switchSnippetView = async (page: Page, view: 'Card view' | 'Table view') = await saved } +const closePreviewIfOpen = async (page: Page) => { + const closeButton = page.getByRole('button', { name: 'Close' }) + + if (await closeButton.isVisible()) { + await closeButton.click() + } +} + const isFeaturedRequest = (url: URL): boolean => url.pathname.includes('/cloud/snippets/featured') || true === url.searchParams.get('rest_route')?.includes('/cloud/snippets/featured') @@ -225,7 +233,7 @@ test.describe('Community Cloud Featured Snippets', () => { await preview.getByRole('button', { name: 'Download' }).click() await expect(preview.getByRole('button', { name: 'Download' })).toBeDisabled() - await expect(card.getByRole('button', { name: 'Download' })).toBeDisabled() + await expect(card.getByRole('button', { name: 'Download', includeHidden: true })).toBeDisabled() releaseDownload() await expect(preview.getByRole('link', { name: 'Edit' })).toBeVisible() @@ -238,7 +246,7 @@ test.describe('Community Cloud Featured Snippets', () => { } finally { releaseDownload() releaseRefresh() - await page.getByRole('button', { name: 'Close' }).click().catch(() => undefined) + await closePreviewIfOpen(page) await switchSnippetView(page, 'Table view').catch(() => undefined) } }) @@ -292,7 +300,7 @@ test.describe('Community Cloud Featured Snippets', () => { await expect(card.getByRole('button', { name: 'Download' })).toHaveCount(0) expect(downloadRequests).toBe(1) } finally { - await page.getByRole('button', { name: 'Close' }).click().catch(() => undefined) + await closePreviewIfOpen(page) await switchSnippetView(page, 'Table view').catch(() => undefined) } }) @@ -303,6 +311,7 @@ test.describe('Community Cloud Featured Snippets', () => { body: JSON.stringify(makeFeaturedResponse()) })) await page.reload() + await switchSnippetView(page, 'Table view') const table = page.locator('.cloud-snippets-table') await expect(table).toBeVisible({ timeout: TIMEOUTS.DEFAULT }) From 910c91ec383e1af7e6d99e2ce4759f1befd10150 Mon Sep 17 00:00:00 2001 From: Imants Date: Fri, 24 Jul 2026 17:57:20 +0300 Subject: [PATCH 14/14] fix: scope cloud action assertions --- .../e2e/code-snippets-community-featured.spec.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/e2e/code-snippets-community-featured.spec.ts b/tests/e2e/code-snippets-community-featured.spec.ts index f066bb4d3..8574e4d9d 100644 --- a/tests/e2e/code-snippets-community-featured.spec.ts +++ b/tests/e2e/code-snippets-community-featured.spec.ts @@ -227,21 +227,25 @@ test.describe('Community Cloud Featured Snippets', () => { try { const card = page.locator('.cloud-search-result', { hasText: 'Downloadable Cloud Snippet' }) + const cardActions = card.locator('.snippet-card-footer-actions') await card.getByRole('button', { name: 'Downloadable Cloud Snippet' }).click() const preview = page.locator('.code-snippets-preview-modal') await preview.getByRole('button', { name: 'Download' }).click() await expect(preview.getByRole('button', { name: 'Download' })).toBeDisabled() - await expect(card.getByRole('button', { name: 'Download', includeHidden: true })).toBeDisabled() + await expect(cardActions.getByRole( + 'button', + { name: 'Download', exact: true, includeHidden: true } + )).toBeDisabled() releaseDownload() await expect(preview.getByRole('link', { name: 'Edit' })).toBeVisible() await expect.poll(() => featuredRequests).toBe(2) await page.getByRole('button', { name: 'Close' }).click() - await expect(card.getByRole('link', { name: 'Edit' })).toBeVisible() - await expect(card.getByRole('button', { name: 'Download' })).toHaveCount(0) + await expect(cardActions.getByRole('link', { name: 'Edit', exact: true })).toBeVisible() + await expect(cardActions.getByRole('button', { name: 'Download', exact: true })).toHaveCount(0) expect(downloadRequests).toBe(1) } finally { releaseDownload() @@ -284,6 +288,7 @@ test.describe('Community Cloud Featured Snippets', () => { try { const card = page.locator('.cloud-search-result', { hasText: 'Downloadable Cloud Snippet' }) + const cardActions = card.locator('.snippet-card-footer-actions') await card.getByRole('button', { name: 'Downloadable Cloud Snippet' }).click() const preview = page.locator('.code-snippets-preview-modal') @@ -296,8 +301,8 @@ test.describe('Community Cloud Featured Snippets', () => { .getByRole('button', { name: 'Search Cloud Library' }) .click() - await expect(card.getByRole('link', { name: 'Edit' })).toBeVisible() - await expect(card.getByRole('button', { name: 'Download' })).toHaveCount(0) + await expect(cardActions.getByRole('link', { name: 'Edit', exact: true })).toBeVisible() + await expect(cardActions.getByRole('button', { name: 'Download', exact: true })).toHaveCount(0) expect(downloadRequests).toBe(1) } finally { await closePreviewIfOpen(page)