6.7 KiB
spec, created, status
| spec | created | status |
|---|---|---|
| docs/specs/plugins/requirements/plugins.md | 2026-08-08 | complete |
Implementation Plan: Plugin install loading state
Overview
The install action already owns a single installBusy state for URL and upload
installs, and the dialog already disables its submit button while that state is
true. The regression is presentational: the dialog always renders the idle
Install label and no loading indicator, so a user cannot tell whether the
disabled action is still working. The fix keeps the existing install pipeline
and dialog composition, rendering the shared busy state as an animated spinner,
an installing label, and an accessible busy state.
Confirmed root cause
usePluginActions.runInstall sets installBusy before invoking either install
API and clears it in finally, but InstallPluginDialog only consumes busy
for disabled={...}. Its primary button always renders t("plugins:install")
and has no icon or aria-busy state. The existing page test passes 16 tests but
does not hold the install promise pending, so it cannot detect this missing
feedback.
Frontend
Install dialog
apps/web/components/settings/plugins/install-plugin-dialog.tsx- Reuse the existing
busyprop for both URL and upload tabs. - Render
IconLoader2withanimate-spinwhile busy and keep it hidden when idle. - Use the existing
plugins:installingtranslation while busy, otherwise keepplugins:install. - Expose
aria-busywhile the pipeline is pending and preserve the stable tab-specific test IDs and disabled behavior.
- Reuse the existing
No backend, API, store, plugin contract, persistence, or marketplace changes are needed. The scope is the manual install dialog shown in the reported screenshot; marketplace actions already expose their own in-progress label and are unchanged.
Mobile parity contract
- Desktop outcome / mobile entry: both viewports submit the same URL/upload install through Settings > Plugins; mobile enters through the existing Plugins settings route and install dialog.
- Nearest shipped exemplar: the existing Radix install dialog and
apps/web/e2e/tests/plugins/mobile-plugin-nav.spec.tsplugin-settings flow. They provide the dialog presentation, footer action, and mobile reachability. - Hierarchy and primary action: the dialog remains the focused surface; the footer Install action remains the primary action and shows the same spinner and installing label on phone and desktop.
- Presentation / scroll / safe area: no composition changes; keep the current Dialog, its existing viewport behavior, and existing scroll ownership. No new fixed control or safe-area handling is introduced.
- Shared state:
installBusy, error handling, and submit callbacks remain shared; no mobile-specific business logic is added. - Mobile proof: extend the existing mobile plugin install flow with a held install response assertion. That flow already owns the disposable plugin fixture and cleanup, so it can prove the same primary action without creating a new mobile surface.
Tests
- What: While a URL or upload install promise is pending, the dialog’s primary
action is disabled, has
aria-busy="true", shows an animated spinner, and uses the installing label; after rejection the spinner disappears and the existing error/retry state remains available. File:apps/web/app/settings/plugins/page.test.tsx. How: hold a mocked install promise with an explicit resolver, assert the rendered button state before resolving/rejecting, then assert the settled state. - What: Existing URL and upload success paths still close the dialog, update
the store, and load the installed bundle.
File:
apps/web/app/settings/plugins/page.test.tsx. How: run the existing focused page suite unchanged alongside the new pending-state regression.
E2E Tests
- Scenario: GIVEN an upload in Settings > Plugins, WHEN the install request
is deliberately held before its response reaches the browser, THEN the
desktop Install button is disabled and visibly spinning; after a controlled
failure is released, the action recovers and the inline error remains visible.
Files:
apps/web/e2e/helpers/plugin-install.tsandapps/web/e2e/tests/plugins/plugins.spec.ts. - Scenario: The same pending upload state remains reachable and visually
understandable on the configured Pixel 5 project.
File:
apps/web/e2e/tests/plugins/mobile-plugin-nav.spec.ts, using the shared install-response helper. - Focused commands:
cd apps && pnpm install --frozen-lockfile(only if this worktree lacks dependencies)cd apps && pnpm --filter @kandev/web test -- --run app/settings/plugins/page.test.tsxcd apps/web && pnpm e2e:run --project chromium e2e/tests/plugins/plugins.spec.ts -- --grep "install.*spinner|spinner.*install"cd apps/web && pnpm e2e:run --project mobile-chrome e2e/tests/plugins/mobile-plugin-nav.spec.ts -- --grep "install.*spinner|spinner.*install"cd apps/web && pnpm run typecheck
Verification Results
make build-backend build-web— passed; Vite production assets rebuilt.cd apps && pnpm --filter @kandev/web test -- --run app/settings/plugins/page.test.tsx— passed, 17 tests.cd apps/web && pnpm e2e:run --host --no-build e2e/tests/plugins/plugins.spec.ts -- --grep "install.*spinner|spinner.*install"— passed, 1 Chromium test.cd apps/web && pnpm e2e:run --host --no-build --project mobile-chrome e2e/tests/plugins/mobile-plugin-nav.spec.ts -- --grep "install.*spinner|spinner.*install"— passed, 1 Pixel 5 test.cd apps/web && pnpm run typecheck— passed.- Targeted ESLint for the changed component, page test, and desktop/mobile plugin specs — passed with zero warnings.
git diff --check— passed.- Interactive dev-browser validation confirmed
disabled,aria-busy="true", the animated spinner, andInstalling…before the held response was released. - Managed screenshot capture passed and generated two fresh, compressed, secret-free assets in
.pr-assets/manifest.jsonfor desktop and Pixel 5 viewports; the disposable capture spec was removed afterward. - CodeRabbit's valid duplication finding was fixed by moving the held-install
route helper into
apps/web/e2e/helpers/plugin-install.ts; both focused E2E regressions passed again afterward.
Implementation Waves And Parallel Candidates
Wave 1 (sequential):
The dialog, page regression test, and focused desktop/mobile E2E evidence share the same install state and should be implemented and verified as one vertical slice. The marketplace’s separate install/update buttons are not changed by this fix.