Compare commits
No commits in common. "e1cf2970da0dcf218c245dab3d163bbe6241ab7d" and "6e06a67bded6b435b13a1b7b25e480fb920f626c" have entirely different histories.
e1cf2970da
...
6e06a67bde
@ -1,179 +0,0 @@
|
|||||||
# Popup Update Panel Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Redesign the extension popup into a compact enterprise-style tool panel with clearer grouping, better spacing, and more readable update actions while keeping existing behavior unchanged.
|
|
||||||
|
|
||||||
**Architecture:** Keep the popup behavior in `src/popup/index.ts` and focus most UI changes in `src/popup/view.ts`. Add only the minimum structural changes needed to support grouped cards, clearer update states, and button hierarchy, then verify the existing popup tests still cover the update workflow.
|
|
||||||
|
|
||||||
**Tech Stack:** TypeScript, Chrome MV3 popup UI, Vitest, jsdom
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Map
|
|
||||||
|
|
||||||
- Modify: `src/popup/view.ts`
|
|
||||||
- Replace the current long text flow with grouped cards and clearer status blocks.
|
|
||||||
- Modify: `src/popup/index.html`
|
|
||||||
- Add popup-level CSS for layout, spacing, typography, and button hierarchy.
|
|
||||||
- Modify: `src/popup/index.ts`
|
|
||||||
- Keep logic unchanged unless the new view structure needs small status-render support changes.
|
|
||||||
- Modify: `tests/popup-entry.test.ts`
|
|
||||||
- Update popup rendering expectations for the redesigned layout.
|
|
||||||
|
|
||||||
### Task 1: Lock the desired popup structure in tests
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `tests/popup-entry.test.ts`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Write the failing layout assertions**
|
|
||||||
|
|
||||||
Update the logged-in and update-available popup tests to assert the new structure exists. Add checks for:
|
|
||||||
|
|
||||||
- a compact product header
|
|
||||||
- an account card/root section
|
|
||||||
- an update card/root section
|
|
||||||
- a primary update button and secondary guide button
|
|
||||||
|
|
||||||
Use selectors that match the intended new DOM shape, for example:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
expect(
|
|
||||||
dom.window.document.querySelector('[data-popup-account="card"]')
|
|
||||||
).not.toBeNull();
|
|
||||||
expect(
|
|
||||||
dom.window.document.querySelector('[data-popup-update="card"]')
|
|
||||||
).not.toBeNull();
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Run the focused popup test**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm test -- tests/popup-entry.test.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: FAIL because the current popup view does not expose the new grouped structure.
|
|
||||||
|
|
||||||
### Task 2: Redesign the logged-in popup shell
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `src/popup/view.ts`
|
|
||||||
- Modify: `src/popup/index.html`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Implement the grouped popup shell**
|
|
||||||
|
|
||||||
Change the logged-in renderer so it produces:
|
|
||||||
|
|
||||||
- a compact header
|
|
||||||
- an account card
|
|
||||||
- an update card container
|
|
||||||
- a low-emphasis footer action area
|
|
||||||
|
|
||||||
Keep the same text content, just reorganized.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Add popup CSS**
|
|
||||||
|
|
||||||
Add scoped CSS in `src/popup/index.html` for:
|
|
||||||
|
|
||||||
- wider popup body
|
|
||||||
- neutral background
|
|
||||||
- white cards
|
|
||||||
- consistent spacing
|
|
||||||
- smaller title scale
|
|
||||||
- primary / secondary / tertiary button styles
|
|
||||||
|
|
||||||
Do not add animation-heavy or branding-heavy styles.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Re-run popup tests**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm test -- tests/popup-entry.test.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: PASS for structure-related checks, with any remaining failures isolated to update-state details.
|
|
||||||
|
|
||||||
### Task 3: Redesign update-state rendering
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `src/popup/view.ts`
|
|
||||||
- Modify: `src/popup/index.ts` (only if required)
|
|
||||||
|
|
||||||
- [ ] **Step 1: Write or update state-specific assertions**
|
|
||||||
|
|
||||||
Ensure tests cover:
|
|
||||||
|
|
||||||
- `checking` state shows a compact progress line
|
|
||||||
- `latest` state hides download actions
|
|
||||||
- `available` state shows current version, latest version, notes, and action buttons
|
|
||||||
- `error` state renders a readable warning block
|
|
||||||
|
|
||||||
- [ ] **Step 2: Run the focused tests**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm test -- tests/popup-entry.test.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: FAIL on any states not yet aligned to the new view.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Implement state-specific card rendering**
|
|
||||||
|
|
||||||
Refactor `renderUpdateStatus()` to keep one card shell and swap state bodies inside it. Make the available-update state visually prominent but restrained.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Verify**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm test -- tests/popup-entry.test.ts tests/update-check.test.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: PASS.
|
|
||||||
|
|
||||||
### Task 4: Verify the popup still works end-to-end
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `src/popup/view.ts` if polish is needed
|
|
||||||
- Modify: `src/popup/index.html` if polish is needed
|
|
||||||
|
|
||||||
- [ ] **Step 1: Build the release popup**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build:release
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: PASS and updated popup assets written to `dist/`.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Run the full test suite**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm test
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: PASS.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Manual smoke check**
|
|
||||||
|
|
||||||
Open the unpacked extension popup from `dist/` and confirm:
|
|
||||||
|
|
||||||
- title no longer wraps into a giant stacked block
|
|
||||||
- account status is easy to scan
|
|
||||||
- update information reads clearly
|
|
||||||
- buttons are visually distinct and not cramped
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add src/popup/view.ts src/popup/index.html src/popup/index.ts tests/popup-entry.test.ts
|
|
||||||
git commit -m "feat: redesign popup update panel"
|
|
||||||
```
|
|
||||||
|
|
||||||
@ -1,100 +0,0 @@
|
|||||||
# Popup Update Panel Design
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Redesign the extension popup so it feels like a compact internal tool instead of a stretched document. The new popup should prioritize readability, clear grouping, and obvious update actions while keeping the interaction model unchanged.
|
|
||||||
|
|
||||||
## Confirmed Direction
|
|
||||||
|
|
||||||
- Visual style: enterprise tool
|
|
||||||
- Primary problem to solve: information feels crowded and hard to read
|
|
||||||
- Layout freedom: use whatever structure best improves clarity
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This change only redesigns the popup presentation layer:
|
|
||||||
|
|
||||||
- improve popup layout
|
|
||||||
- improve spacing and typography
|
|
||||||
- improve button hierarchy
|
|
||||||
- improve update-status presentation
|
|
||||||
|
|
||||||
This change does not alter:
|
|
||||||
|
|
||||||
- auth flow
|
|
||||||
- update-check logic
|
|
||||||
- download behavior
|
|
||||||
- background message behavior
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
Use a grouped card layout instead of one long text column.
|
|
||||||
|
|
||||||
Recommended structure:
|
|
||||||
|
|
||||||
1. Header
|
|
||||||
- compact product title on one line
|
|
||||||
2. Account card
|
|
||||||
- login state
|
|
||||||
- current user name
|
|
||||||
3. Update card
|
|
||||||
- current version
|
|
||||||
- latest version or current status
|
|
||||||
- release notes
|
|
||||||
- primary and secondary action buttons
|
|
||||||
- short follow-up instruction text
|
|
||||||
4. Footer action
|
|
||||||
- low-emphasis sign-out button
|
|
||||||
|
|
||||||
## Visual Rules
|
|
||||||
|
|
||||||
- Increase popup width to a more usable fixed tool width.
|
|
||||||
- Use a soft neutral page background with white content cards.
|
|
||||||
- Reduce title size significantly from the current oversized stacked text.
|
|
||||||
- Use a clear type scale:
|
|
||||||
- product title: medium emphasis
|
|
||||||
- section title: medium emphasis
|
|
||||||
- status/version rows: normal emphasis
|
|
||||||
- helper text: smaller and lighter
|
|
||||||
- Keep the palette restrained and neutral.
|
|
||||||
- Make `下载更新包` the primary button.
|
|
||||||
- Make `下载使用说明` a secondary button.
|
|
||||||
- Make `退出登录` a tertiary or low-emphasis button.
|
|
||||||
|
|
||||||
## Update State Presentation
|
|
||||||
|
|
||||||
- `checking`: show a compact in-progress line inside the update card.
|
|
||||||
- `latest`: show a simple success-style status without action buttons.
|
|
||||||
- `available`: show a clear “发现新版本” status block with versions and actions.
|
|
||||||
- `error`: show a readable warning block instead of generic broken-looking text flow.
|
|
||||||
|
|
||||||
## Content Rules
|
|
||||||
|
|
||||||
- Keep product name to one visual line or two short lines max.
|
|
||||||
- Avoid large paragraphs in the popup.
|
|
||||||
- Release notes should stay as a compact bullet list.
|
|
||||||
- The post-download instruction should be one concise sentence.
|
|
||||||
- Version labels should align consistently so the user can compare current/latest quickly.
|
|
||||||
|
|
||||||
## Implementation Notes
|
|
||||||
|
|
||||||
- Keep the existing popup DOM entrypoints and rendering flow.
|
|
||||||
- Focus changes in `src/popup/view.ts` first.
|
|
||||||
- Only adjust popup controller code if needed to support cleaner status rendering.
|
|
||||||
- Prefer CSS embedded in the popup HTML/view flow only as needed; do not expand scope into unrelated refactors.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Add or update popup rendering tests for:
|
|
||||||
|
|
||||||
- logged-in layout still renders correctly
|
|
||||||
- available update state still shows current/latest versions
|
|
||||||
- update action buttons still exist
|
|
||||||
- latest/error states still render expected text
|
|
||||||
|
|
||||||
## Out of Scope
|
|
||||||
|
|
||||||
- new popup features
|
|
||||||
- star chart page banners
|
|
||||||
- animation-heavy UI
|
|
||||||
- branding-heavy marketing visuals
|
|
||||||
@ -4,183 +4,6 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Star Chart Search Enhancer</title>
|
<title>Star Chart Search Enhancer</title>
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
color-scheme: light;
|
|
||||||
--popup-bg: #f3f4f6;
|
|
||||||
--popup-card: #ffffff;
|
|
||||||
--popup-text: #111827;
|
|
||||||
--popup-subtle: #6b7280;
|
|
||||||
--popup-border: #d1d5db;
|
|
||||||
--popup-accent: #8f1f4b;
|
|
||||||
--popup-accent-strong: #74183b;
|
|
||||||
--popup-success: #065f46;
|
|
||||||
--popup-warning: #92400e;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
width: 380px;
|
|
||||||
min-height: 560px;
|
|
||||||
margin: 0;
|
|
||||||
background: var(--popup-bg);
|
|
||||||
color: var(--popup-text);
|
|
||||||
font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-shell {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-header {
|
|
||||||
padding: 6px 2px 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-eyebrow {
|
|
||||||
margin: 0 0 6px;
|
|
||||||
color: var(--popup-subtle);
|
|
||||||
font-size: 12px;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-header h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1.08;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: -0.03em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-card {
|
|
||||||
background: var(--popup-card);
|
|
||||||
border: 1px solid rgba(17, 24, 39, 0.08);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 14px;
|
|
||||||
box-shadow: 0 10px 24px rgba(17, 24, 39, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-card-title {
|
|
||||||
margin: 0 0 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--popup-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-status {
|
|
||||||
margin: 0 0 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-status--accent {
|
|
||||||
color: var(--popup-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-user {
|
|
||||||
margin: 0 0 2px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-copy,
|
|
||||||
.popup-error,
|
|
||||||
.popup-warning,
|
|
||||||
.popup-success {
|
|
||||||
margin: 0 0 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: var(--popup-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-error {
|
|
||||||
color: #b42318;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-warning {
|
|
||||||
color: var(--popup-warning);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-success {
|
|
||||||
color: var(--popup-success);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-notes {
|
|
||||||
margin: 0 0 12px;
|
|
||||||
padding-left: 18px;
|
|
||||||
color: var(--popup-text);
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-notes li + li {
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-button {
|
|
||||||
appearance: none;
|
|
||||||
border: 1px solid var(--popup-border);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 9px 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-button--primary {
|
|
||||||
border-color: var(--popup-accent);
|
|
||||||
background: var(--popup-accent);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-button--primary:hover {
|
|
||||||
background: var(--popup-accent-strong);
|
|
||||||
border-color: var(--popup-accent-strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-button--secondary {
|
|
||||||
background: #fff;
|
|
||||||
color: var(--popup-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-button--secondary:hover,
|
|
||||||
.popup-button--ghost:hover {
|
|
||||||
background: rgba(17, 24, 39, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-button--ghost {
|
|
||||||
background: transparent;
|
|
||||||
color: var(--popup-subtle);
|
|
||||||
border-color: transparent;
|
|
||||||
padding-inline: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main id="app"></main>
|
<main id="app"></main>
|
||||||
|
|||||||
@ -104,7 +104,7 @@ async function renderCurrentAuthState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderLoggedIn(root, response.value);
|
renderLoggedIn(root, response.value);
|
||||||
await runUpdateCheck(root, sendMessage, updateOptions);
|
void runUpdateCheck(root, sendMessage, updateOptions);
|
||||||
root
|
root
|
||||||
.querySelector('[data-popup-sign-out="button"]')
|
.querySelector('[data-popup-sign-out="button"]')
|
||||||
?.addEventListener("click", () => {
|
?.addEventListener("click", () => {
|
||||||
@ -195,10 +195,9 @@ async function runUpdateCheck(
|
|||||||
status: "available"
|
status: "available"
|
||||||
});
|
});
|
||||||
bindUpdateDownloadButtons(root, sendMessage, manifest);
|
bindUpdateDownloadButtons(root, sendMessage, manifest);
|
||||||
} catch (error) {
|
} catch {
|
||||||
renderUpdateStatus(root, {
|
renderUpdateStatus(root, {
|
||||||
currentVersion: options.currentVersion,
|
currentVersion: options.currentVersion,
|
||||||
message: error instanceof Error ? error.message : String(error),
|
|
||||||
status: "error"
|
status: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,18 +3,11 @@ import type { UpdateManifest } from "../shared/update-check";
|
|||||||
|
|
||||||
export function renderLoggedOut(root: HTMLElement, error?: string | null): void {
|
export function renderLoggedOut(root: HTMLElement, error?: string | null): void {
|
||||||
root.innerHTML = `
|
root.innerHTML = `
|
||||||
<section class="popup-shell" data-popup-shell="root" data-popup-state="logged-out">
|
<section data-popup-state="logged-out">
|
||||||
<header class="popup-header" data-popup-header="root">
|
<h1>Star Chart Search Enhancer</h1>
|
||||||
<p class="popup-eyebrow">内部工具</p>
|
<p>登录后才能使用星图增强功能</p>
|
||||||
<h1>Star Chart Search Enhancer</h1>
|
${error ? `<p data-popup-error="true">${error}</p>` : ""}
|
||||||
</header>
|
<button type="button" data-popup-sign-in="button">登录 Logto</button>
|
||||||
<section class="popup-card popup-card--account" data-popup-account="card">
|
|
||||||
<div class="popup-card-title">登录状态</div>
|
|
||||||
<p class="popup-status">未登录</p>
|
|
||||||
<p class="popup-copy">登录后才能使用星图增强功能</p>
|
|
||||||
${error ? `<p class="popup-error" data-popup-error="true">${escapeHtml(error)}</p>` : ""}
|
|
||||||
<button type="button" class="popup-button popup-button--primary" data-popup-sign-in="button">登录 Logto</button>
|
|
||||||
</section>
|
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -26,24 +19,16 @@ export function renderLoggedIn(
|
|||||||
const userInfo = authState.userInfo;
|
const userInfo = authState.userInfo;
|
||||||
|
|
||||||
root.innerHTML = `
|
root.innerHTML = `
|
||||||
<section class="popup-shell" data-popup-shell="root" data-popup-state="logged-in">
|
<section data-popup-state="logged-in">
|
||||||
<header class="popup-header" data-popup-header="root">
|
<h1>Star Chart Search Enhancer</h1>
|
||||||
<p class="popup-eyebrow">内部工具</p>
|
<p>已登录</p>
|
||||||
<h1>Star Chart Search Enhancer</h1>
|
<p>${userInfo?.name ?? userInfo?.username ?? "未知用户"}</p>
|
||||||
</header>
|
<p>${userInfo?.email ?? ""}</p>
|
||||||
<section class="popup-card popup-card--account" data-popup-account="card">
|
<section data-popup-update="root">
|
||||||
<div class="popup-card-title">登录状态</div>
|
<h2>版本更新</h2>
|
||||||
<p class="popup-status">已登录</p>
|
<p data-popup-update-status="text">正在检查更新...</p>
|
||||||
<p class="popup-user">${escapeHtml(userInfo?.name ?? userInfo?.username ?? "未知用户")}</p>
|
|
||||||
<p class="popup-copy">${escapeHtml(userInfo?.email ?? "")}</p>
|
|
||||||
</section>
|
</section>
|
||||||
<section class="popup-card popup-card--update" data-popup-update="card" data-popup-update-root="root">
|
<button type="button" data-popup-sign-out="button">退出登录</button>
|
||||||
<div class="popup-card-title">版本更新</div>
|
|
||||||
<p data-popup-update-status="text" class="popup-status">正在检查更新...</p>
|
|
||||||
</section>
|
|
||||||
<footer class="popup-footer">
|
|
||||||
<button type="button" class="popup-button popup-button--ghost" data-popup-sign-out="button">退出登录</button>
|
|
||||||
</footer>
|
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -53,54 +38,50 @@ export function renderUpdateStatus(
|
|||||||
options: {
|
options: {
|
||||||
currentVersion: string;
|
currentVersion: string;
|
||||||
manifest?: UpdateManifest;
|
manifest?: UpdateManifest;
|
||||||
message?: string | null;
|
|
||||||
status: "checking" | "error" | "latest" | "available";
|
status: "checking" | "error" | "latest" | "available";
|
||||||
}
|
}
|
||||||
): void {
|
): void {
|
||||||
const container = root.querySelector('[data-popup-update-root="root"]');
|
const container = root.querySelector('[data-popup-update="root"]');
|
||||||
if (!container) {
|
if (!container) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.status === "checking") {
|
if (options.status === "checking") {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="popup-card-title">版本更新</div>
|
<h2>版本更新</h2>
|
||||||
<p data-popup-update-status="text" class="popup-status">当前版本:${options.currentVersion}</p>
|
<p data-popup-update-status="text">当前版本:${options.currentVersion}</p>
|
||||||
<p class="popup-copy">正在检查更新...</p>
|
<p>正在检查更新...</p>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.status === "error") {
|
if (options.status === "error") {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="popup-card-title">版本更新</div>
|
<h2>版本更新</h2>
|
||||||
<p data-popup-update-status="text" class="popup-status">当前版本:${options.currentVersion}</p>
|
<p data-popup-update-status="text">当前版本:${options.currentVersion}</p>
|
||||||
<p class="popup-warning">暂时无法检查更新</p>
|
<p>暂时无法检查更新</p>
|
||||||
${options.message ? `<p class="popup-error">${escapeHtml(options.message)}</p>` : ""}
|
<p>如果需要新版,请联系维护同事获取更新包。</p>
|
||||||
<p class="popup-copy">如果需要新版,请联系维护同事获取更新包。</p>
|
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.status === "latest" || !options.manifest) {
|
if (options.status === "latest" || !options.manifest) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="popup-card-title">版本更新</div>
|
<h2>版本更新</h2>
|
||||||
<p data-popup-update-status="text" class="popup-status">当前版本:${options.currentVersion}</p>
|
<p data-popup-update-status="text">当前版本:${options.currentVersion}</p>
|
||||||
<p class="popup-success">当前已是最新版本</p>
|
<p>当前已是最新版本</p>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="popup-card-title">版本更新</div>
|
<h2>版本更新</h2>
|
||||||
<p data-popup-update-status="text" class="popup-status">当前版本:${options.currentVersion}</p>
|
<p data-popup-update-status="text">当前版本:${options.currentVersion}</p>
|
||||||
<p class="popup-status popup-status--accent">发现新版本:${options.manifest.latestVersion}</p>
|
<p>发现新版本:${options.manifest.latestVersion}</p>
|
||||||
${renderReleaseNotes(options.manifest.releaseNotes)}
|
${renderReleaseNotes(options.manifest.releaseNotes)}
|
||||||
<div class="popup-actions">
|
<button type="button" data-popup-download-update="button">下载更新包</button>
|
||||||
<button type="button" class="popup-button popup-button--primary" data-popup-download-update="button">下载更新包</button>
|
<button type="button" data-popup-download-guide="button">下载使用说明</button>
|
||||||
<button type="button" class="popup-button popup-button--secondary" data-popup-download-guide="button">下载使用说明</button>
|
<p data-popup-update-download-status="text">下载后请解压新版 zip,并在 chrome://extensions 里重新加载插件。</p>
|
||||||
</div>
|
|
||||||
<p data-popup-update-download-status="text" class="popup-copy">下载后请解压新版 zip,并在 chrome://extensions 里重新加载插件。</p>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,7 +103,7 @@ function renderReleaseNotes(releaseNotes: string[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<ul class="popup-notes">
|
<ul>
|
||||||
${releaseNotes.map((note) => `<li>${escapeHtml(note)}</li>`).join("")}
|
${releaseNotes.map((note) => `<li>${escapeHtml(note)}</li>`).join("")}
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
@ -141,16 +122,15 @@ export function renderDevPanel(
|
|||||||
authState: AuthStateValue
|
authState: AuthStateValue
|
||||||
): void {
|
): void {
|
||||||
const panel = root.ownerDocument.createElement("section");
|
const panel = root.ownerDocument.createElement("section");
|
||||||
panel.className = "popup-card popup-card--dev";
|
|
||||||
panel.dataset.popupDevPanel = "root";
|
panel.dataset.popupDevPanel = "root";
|
||||||
panel.innerHTML = `
|
panel.innerHTML = `
|
||||||
<div class="popup-card-title">dev auth panel</div>
|
<h2>dev auth panel</h2>
|
||||||
<p class="popup-copy">resource: ${escapeHtml(authState.resource ?? "")}</p>
|
<p>resource: ${authState.resource ?? ""}</p>
|
||||||
<p class="popup-copy">scopes: ${escapeHtml((authState.scopes ?? []).join(", "))}</p>
|
<p>scopes: ${(authState.scopes ?? []).join(", ")}</p>
|
||||||
<p class="popup-copy">token: ${authState.tokenAvailable ? "available" : "missing"}</p>
|
<p>token: ${authState.tokenAvailable ? "available" : "missing"}</p>
|
||||||
<p class="popup-copy">expires: ${escapeHtml(String(authState.accessTokenExpiresAt ?? "unknown"))}</p>
|
<p>expires: ${authState.accessTokenExpiresAt ?? "unknown"}</p>
|
||||||
<p class="popup-copy">error: ${escapeHtml(authState.lastError ?? "")}</p>
|
<p>error: ${authState.lastError ?? ""}</p>
|
||||||
<button type="button" class="popup-button popup-button--secondary" data-popup-test-protected-api="button">测试受保护接口</button>
|
<button type="button" data-popup-test-protected-api="button">测试受保护接口</button>
|
||||||
<pre data-popup-protected-api-result="output"></pre>
|
<pre data-popup-protected-api-result="output"></pre>
|
||||||
`;
|
`;
|
||||||
root.appendChild(panel);
|
root.appendChild(panel);
|
||||||
|
|||||||
@ -3,10 +3,6 @@ import { beforeEach, describe, expect, test, vi } from "vitest";
|
|||||||
|
|
||||||
import { bootPopup } from "../src/popup/index";
|
import { bootPopup } from "../src/popup/index";
|
||||||
|
|
||||||
function flushTasks(): Promise<void> {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("popup-entry", () => {
|
describe("popup-entry", () => {
|
||||||
let dom: JSDOM;
|
let dom: JSDOM;
|
||||||
|
|
||||||
@ -26,12 +22,6 @@ describe("popup-entry", () => {
|
|||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
|
||||||
dom.window.document.querySelector('[data-popup-shell="root"]')
|
|
||||||
).not.toBeNull();
|
|
||||||
expect(
|
|
||||||
dom.window.document.querySelector('[data-popup-account="card"]')
|
|
||||||
).not.toBeNull();
|
|
||||||
expect(dom.window.document.querySelector("button")?.textContent).toContain(
|
expect(dom.window.document.querySelector("button")?.textContent).toContain(
|
||||||
"登录"
|
"登录"
|
||||||
);
|
);
|
||||||
@ -57,12 +47,6 @@ describe("popup-entry", () => {
|
|||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
|
||||||
dom.window.document.querySelector('[data-popup-header="root"]')
|
|
||||||
).not.toBeNull();
|
|
||||||
expect(
|
|
||||||
dom.window.document.querySelector('[data-popup-account="card"]')
|
|
||||||
).not.toBeNull();
|
|
||||||
expect(dom.window.document.body.textContent).toContain("resource");
|
expect(dom.window.document.body.textContent).toContain("resource");
|
||||||
expect(dom.window.document.body.textContent).toContain("token");
|
expect(dom.window.document.body.textContent).toContain("token");
|
||||||
});
|
});
|
||||||
@ -92,16 +76,12 @@ describe("popup-entry", () => {
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
await flushTasks();
|
await Promise.resolve();
|
||||||
await flushTasks();
|
|
||||||
|
|
||||||
expect(fetchUpdateManifest).toHaveBeenCalledTimes(1);
|
expect(fetchUpdateManifest).toHaveBeenCalledTimes(1);
|
||||||
expect(dom.window.document.body.textContent).toContain("当前版本:0.2.0421.2");
|
expect(dom.window.document.body.textContent).toContain("当前版本:0.2.0421.2");
|
||||||
expect(dom.window.document.body.textContent).toContain("发现新版本:0.2.0421.3");
|
expect(dom.window.document.body.textContent).toContain("发现新版本:0.2.0421.3");
|
||||||
expect(dom.window.document.body.textContent).toContain("支持检查更新");
|
expect(dom.window.document.body.textContent).toContain("支持检查更新");
|
||||||
expect(
|
|
||||||
dom.window.document.querySelector('[data-popup-update="card"]')
|
|
||||||
).not.toBeNull();
|
|
||||||
expect(
|
expect(
|
||||||
dom.window.document.querySelector('[data-popup-download-update="button"]')
|
dom.window.document.querySelector('[data-popup-download-update="button"]')
|
||||||
).not.toBeNull();
|
).not.toBeNull();
|
||||||
@ -138,8 +118,7 @@ describe("popup-entry", () => {
|
|||||||
})),
|
})),
|
||||||
sendMessage
|
sendMessage
|
||||||
});
|
});
|
||||||
await flushTasks();
|
await Promise.resolve();
|
||||||
await flushTasks();
|
|
||||||
|
|
||||||
(
|
(
|
||||||
dom.window.document.querySelector(
|
dom.window.document.querySelector(
|
||||||
@ -266,63 +245,6 @@ describe("popup-entry", () => {
|
|||||||
expect(sendMessage).toHaveBeenCalledWith({ type: "auth:sign-out" });
|
expect(sendMessage).toHaveBeenCalledWith({ type: "auth:sign-out" });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("shows the latest state without update actions", async () => {
|
|
||||||
dom.window.document.body.innerHTML = "<main id='app'></main>";
|
|
||||||
|
|
||||||
await bootPopup({
|
|
||||||
currentVersion: "0.0525.5",
|
|
||||||
document: dom.window.document,
|
|
||||||
fetchUpdateManifest: vi.fn(async () => ({
|
|
||||||
guideUrl: "https://cos.example.com/guide.pdf",
|
|
||||||
latestVersion: "0.0525.5",
|
|
||||||
minSupportedVersion: "0.0525.5",
|
|
||||||
publishedAt: "2026-05-19",
|
|
||||||
releaseNotes: ["支持检查更新"],
|
|
||||||
zipUrl: "https://cos.example.com/plugin.zip"
|
|
||||||
})),
|
|
||||||
sendMessage: vi.fn(async () => ({
|
|
||||||
ok: true,
|
|
||||||
type: "auth:state",
|
|
||||||
value: {
|
|
||||||
isAuthenticated: true,
|
|
||||||
userInfo: { name: "Dev" }
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
await flushTasks();
|
|
||||||
await flushTasks();
|
|
||||||
|
|
||||||
expect(dom.window.document.body.textContent).toContain("当前已是最新版本");
|
|
||||||
expect(
|
|
||||||
dom.window.document.querySelector('[data-popup-download-update="button"]')
|
|
||||||
).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("shows a readable error state when the manifest fetch fails", async () => {
|
|
||||||
dom.window.document.body.innerHTML = "<main id='app'></main>";
|
|
||||||
|
|
||||||
await bootPopup({
|
|
||||||
currentVersion: "0.0525.5",
|
|
||||||
document: dom.window.document,
|
|
||||||
fetchUpdateManifest: vi.fn(async () => {
|
|
||||||
throw new Error("network down");
|
|
||||||
}),
|
|
||||||
sendMessage: vi.fn(async () => ({
|
|
||||||
ok: true,
|
|
||||||
type: "auth:state",
|
|
||||||
value: {
|
|
||||||
isAuthenticated: true,
|
|
||||||
userInfo: { name: "Dev" }
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
await flushTasks();
|
|
||||||
await flushTasks();
|
|
||||||
|
|
||||||
expect(dom.window.document.body.textContent).toContain("暂时无法检查更新");
|
|
||||||
expect(dom.window.document.body.textContent).toContain("network down");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("shows the auth error when sign-in fails", async () => {
|
test("shows the auth error when sign-in fails", async () => {
|
||||||
const sendMessage = vi
|
const sendMessage = vi
|
||||||
.fn()
|
.fn()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user