fix(enrichment-block): migrate $app/stores → $app/state to eliminate birpc race
The async vi.mock factory in EnrichmentBlock.svelte.spec.ts performed an `await import(...)` in its body — the same mechanism #535/#546 fixed for pdfjs-dist. Issue #553: when Chromium's playwright route handler fetches the mocked module after the worker's birpc channel has closed, the factory's RPC roundtrip raises `[birpc] rpc is closed, cannot call "resolveManualMock"` and the run exits 1. Migrate EnrichmentBlock from the deprecated `$app/stores.navigating` (store) to the modern `$app/state.navigating` (reactive proxy). The spec uses vi.hoisted + a sync vi.mock factory with a getter that defers the read — no dynamic import in the factory body. Delete the now-unused __mocks__/navigatingStore.ts. Fix path applied: $app/state migration (Markus's recommendation / Felix's Path 2). See ADR-012. Refs #553 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { navigating } from '$app/stores';
|
import { navigating } from '$app/state';
|
||||||
import DashboardNeedsMetadata from './DashboardNeedsMetadata.svelte';
|
import DashboardNeedsMetadata from './DashboardNeedsMetadata.svelte';
|
||||||
import UploadSuccessBanner from './UploadSuccessBanner.svelte';
|
import UploadSuccessBanner from './UploadSuccessBanner.svelte';
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ interface Props {
|
|||||||
|
|
||||||
let { topDocs, totalCount, bannerCount, onBannerClose }: Props = $props();
|
let { topDocs, totalCount, bannerCount, onBannerClose }: Props = $props();
|
||||||
|
|
||||||
const showSkeleton = $derived(!!$navigating && topDocs.length === 0);
|
const showSkeleton = $derived(!!navigating.type && topDocs.length === 0);
|
||||||
const showBlock = $derived(topDocs.length > 0 || bannerCount > 0 || showSkeleton);
|
const showBlock = $derived(topDocs.length > 0 || bannerCount > 0 || showSkeleton);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,23 @@ import { describe, it, expect, afterEach, vi } from 'vitest';
|
|||||||
import { cleanup, render } from 'vitest-browser-svelte';
|
import { cleanup, render } from 'vitest-browser-svelte';
|
||||||
import { page } from 'vitest/browser';
|
import { page } from 'vitest/browser';
|
||||||
|
|
||||||
// The store must live in a separate module because vi.mock factories are
|
|
||||||
// hoisted and cannot reference top-level variables defined in this file.
|
|
||||||
import { navigatingStore } from './__mocks__/navigatingStore';
|
|
||||||
import EnrichmentBlock from './EnrichmentBlock.svelte';
|
import EnrichmentBlock from './EnrichmentBlock.svelte';
|
||||||
|
|
||||||
vi.mock('$app/stores', async () => {
|
// Hoist the mutable navigation reference so vi.mock's factory (also hoisted)
|
||||||
const mod = await import('./__mocks__/navigatingStore');
|
// can read it via a getter. Sync factory, no dynamic import: ADR-012 invariant.
|
||||||
return { navigating: mod.navigatingStore };
|
const { mockNavigating } = vi.hoisted(() => ({
|
||||||
});
|
mockNavigating: { type: null as string | null }
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('$app/state', () => ({
|
||||||
|
get navigating() {
|
||||||
|
return mockNavigating;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
navigatingStore.set(null);
|
mockNavigating.type = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
type Doc = { id: string; title: string; uploadedAt: string };
|
type Doc = { id: string; title: string; uploadedAt: string };
|
||||||
@@ -65,8 +69,8 @@ describe('EnrichmentBlock', () => {
|
|||||||
await expect.element(page.getByTestId('dashboard-needs-metadata')).toBeInTheDocument();
|
await expect.element(page.getByTestId('dashboard-needs-metadata')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the skeleton when $navigating is active and topDocs is empty', async () => {
|
it('renders the skeleton when navigation is active and topDocs is empty', async () => {
|
||||||
navigatingStore.set({ type: 'link' });
|
mockNavigating.type = 'link';
|
||||||
render(EnrichmentBlock, {
|
render(EnrichmentBlock, {
|
||||||
topDocs: [],
|
topDocs: [],
|
||||||
totalCount: 0,
|
totalCount: 0,
|
||||||
@@ -76,8 +80,8 @@ describe('EnrichmentBlock', () => {
|
|||||||
await expect.element(page.getByTestId('enrichment-block-skeleton')).toBeInTheDocument();
|
await expect.element(page.getByTestId('enrichment-block-skeleton')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render the skeleton when topDocs is non-empty even during $navigating', async () => {
|
it('does not render the skeleton when topDocs is non-empty even during navigation', async () => {
|
||||||
navigatingStore.set({ type: 'link' });
|
mockNavigating.type = 'link';
|
||||||
render(EnrichmentBlock, {
|
render(EnrichmentBlock, {
|
||||||
topDocs: [doc('d1')],
|
topDocs: [doc('d1')],
|
||||||
totalCount: 1,
|
totalCount: 1,
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import { writable } from 'svelte/store';
|
|
||||||
|
|
||||||
export const navigatingStore = writable<unknown | null>(null);
|
|
||||||
Reference in New Issue
Block a user