diff --git a/index.html b/index.html index d4c9a94..5f45b79 100644 --- a/index.html +++ b/index.html @@ -77,7 +77,54 @@ })(); - + + diff --git a/public/manifest.json b/public/manifest.json index 2a337e7..9fe7284 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -98,9 +98,8 @@ } ], "share_target": { - "action": "/share", - "method": "POST", - "enctype": "multipart/form-data", + "action": "/", + "method": "GET", "params": { "title": "title", "text": "text", diff --git a/src/components/AddFeedDialog/AddFeedDialog.tsx b/src/components/AddFeedDialog/AddFeedDialog.tsx index cb2c8d0..99a9176 100644 --- a/src/components/AddFeedDialog/AddFeedDialog.tsx +++ b/src/components/AddFeedDialog/AddFeedDialog.tsx @@ -3,7 +3,7 @@ * Modal dialog for adding new RSS feed subscriptions */ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useStore } from '../../hooks/useStore'; import { useOfflineDetection } from '../../hooks/useOfflineDetection'; @@ -12,9 +12,10 @@ import { useToast } from '../../hooks/useToast'; interface AddFeedDialogProps { isOpen: boolean; onClose: () => void; + initialUrl?: string; } -export function AddFeedDialog({ isOpen, onClose }: AddFeedDialogProps) { +export function AddFeedDialog({ isOpen, onClose, initialUrl }: AddFeedDialogProps) { const { t } = useTranslation('feed'); const [url, setUrl] = useState(''); const [categoryId, setCategoryId] = useState(''); @@ -30,6 +31,13 @@ export function AddFeedDialog({ isOpen, onClose }: AddFeedDialogProps) { loadCategories(); }); + // Populate URL field when dialog opens with an initial URL (e.g. from share target) + useEffect(() => { + if (isOpen && initialUrl) { + setUrl(initialUrl); + } + }, [isOpen, initialUrl]); + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); diff --git a/src/pages/FeedsPage.tsx b/src/pages/FeedsPage.tsx index 4a06c91..0cd2be5 100644 --- a/src/pages/FeedsPage.tsx +++ b/src/pages/FeedsPage.tsx @@ -38,8 +38,29 @@ export function FeedsPage() { const [isRefreshing, setIsRefreshing] = useState(false); const [articleCounts, setArticleCounts] = useState>({}); const [isBottomBarVisible, setIsBottomBarVisible] = useState(true); + const [sharedFeedUrl, setSharedFeedUrl] = useState(''); const lastScrollY = useRef(0); + // Handle PWA shortcuts (?action=add-feed) and Web Share Target (?url=... or ?text=...) + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const action = params.get('action'); + const sharedUrl = params.get('url') || params.get('text'); + + if (action === 'add-feed') { + openAddFeedDialog(); + } else if (sharedUrl) { + // If the param is plain text with an embedded URL, extract it + let feedUrl = sharedUrl; + if (!/^https?:\/\//i.test(feedUrl)) { + const match = sharedUrl.match(/https?:\/\/[^\s]+/i); + feedUrl = match ? match[0] : sharedUrl; + } + setSharedFeedUrl(feedUrl); + openAddFeedDialog(); + } + }, [openAddFeedDialog]); + // Load feeds, categories and start auto-refresh on mount useEffect(() => { let cancelled = false; @@ -327,7 +348,11 @@ export function FeedsPage() { {/* Add Feed Dialog */} - + { closeAddFeedDialog(); setSharedFeedUrl(''); }} + initialUrl={sharedFeedUrl} + /> ); } \ No newline at end of file diff --git a/tests/e2e/ci/favorites.spec.ts b/tests/e2e/ci/favorites.spec.ts index 5c1e348..fc83627 100644 --- a/tests/e2e/ci/favorites.spec.ts +++ b/tests/e2e/ci/favorites.spec.ts @@ -58,13 +58,21 @@ test.describe('Favorites Functionality', () => { await articleLink.click(); await page.waitForLoadState('networkidle'); - // Click the favorite button in the action bar - const favoriteButton = page.locator('button').filter({ hasText: /Favorite|收藏|favorite/ }).first(); + // If the article is already favorited (from a previous test run), unfavorite it first + // so we can reliably test the favorite→favorited transition + const alreadyFavoritedBtn = page.locator('button').filter({ hasText: /^Favorited$|^已收藏$/ }).first(); + if (await alreadyFavoritedBtn.isVisible({ timeout: 3_000 }).catch(() => false)) { + await alreadyFavoritedBtn.click(); + await expect(page.locator('button').filter({ hasText: /^Favorite$|^收藏$/ }).first()).toBeVisible({ timeout: 10_000 }); + } + + // Click the favorite button in the action bar (exact match to avoid matching "Favorited") + const favoriteButton = page.locator('button').filter({ hasText: /^Favorite$|^收藏$/ }).first(); await expect(favoriteButton).toBeVisible({ timeout: 10_000 }); await favoriteButton.click(); // Wait for the favorited state to appear (text changes to "Favorited" / "已收藏") - await expect(page.locator('button').filter({ hasText: /Favorited|已收藏|favorited/ }).first()).toBeVisible({ timeout: 5_000 }); + await expect(page.locator('button').filter({ hasText: /^Favorited$|^已收藏$/ }).first()).toBeVisible({ timeout: 10_000 }); // Navigate to favorites page await page.goto('/#/favorites'); diff --git a/tests/e2e/ci/feedSubscription.spec.ts b/tests/e2e/ci/feedSubscription.spec.ts index b7c0bd7..64af4d5 100644 --- a/tests/e2e/ci/feedSubscription.spec.ts +++ b/tests/e2e/ci/feedSubscription.spec.ts @@ -171,11 +171,10 @@ test.describe('RSS Feed Subscription', () => { // Navigate back to feeds list const backToFeedsButton = page.locator('button').filter({ hasText: /Back to Feeds|返回订阅列表/ }).first(); await backToFeedsButton.click(); - await page.waitForLoadState('networkidle'); - // Verify we're back at feeds list - const mainHeading = page.locator('h1').first(); - await expect(mainHeading).toBeVisible({ timeout: 10_000 }); + // Verify we're back at feeds list (wait for h1 matching Feeds/订阅源 directly) + const mainHeading = page.locator('h1').filter({ hasText: /Feeds|订阅源/ }).first(); + await expect(mainHeading).toBeVisible({ timeout: 15_000 }); const text = await mainHeading.textContent(); expect(text).toMatch(/Feeds|订阅源/); }); diff --git a/tests/e2e/ci/history.spec.ts b/tests/e2e/ci/history.spec.ts index bbf868a..cfc2e18 100644 --- a/tests/e2e/ci/history.spec.ts +++ b/tests/e2e/ci/history.spec.ts @@ -64,6 +64,8 @@ test.describe('Reading History', () => { // Navigate to history page await page.goto('/#/history'); + // Wait for the loading spinner to disappear before checking content + await page.waitForSelector('.animate-spin', { state: 'hidden', timeout: 10_000 }).catch(() => {}); await page.waitForLoadState('networkidle'); // Verify history page shows the read article @@ -74,7 +76,7 @@ test.describe('Reading History', () => { // Should have at least one article in history (the one we just read) const historyArticles = page.locator('a[href*="/articles/"]'); - await expect(historyArticles.first()).toBeVisible({ timeout: 10_000 }); + await expect(historyArticles.first()).toBeVisible({ timeout: 15_000 }); const count = await historyArticles.count(); expect(count).toBeGreaterThan(0); });