From 3acab745851e3830ebeede2af58c37bcabdde399 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 10:36:05 +0000 Subject: [PATCH 1/2] wip: install explain double in plugin-view overlay-title suites --- .../ObjectView.overlayTitleI18n.test.tsx | 13 +++- ...ew.overlayTitleNoProviderFallback.test.tsx | 13 +++- .../src/__tests__/explainDouble.ts | 76 +++++++++++++++++++ 3 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 packages/plugin-view/src/__tests__/explainDouble.ts diff --git a/packages/plugin-view/src/__tests__/ObjectView.overlayTitleI18n.test.tsx b/packages/plugin-view/src/__tests__/ObjectView.overlayTitleI18n.test.tsx index ad3ea2714b..b322e41606 100644 --- a/packages/plugin-view/src/__tests__/ObjectView.overlayTitleI18n.test.tsx +++ b/packages/plugin-view/src/__tests__/ObjectView.overlayTitleI18n.test.tsx @@ -46,12 +46,13 @@ */ import React from 'react'; -import { describe, it, expect, vi, afterEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, cleanup, fireEvent, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; import { I18nProvider } from '@object-ui/i18n'; import type { DataSource } from '@object-ui/types'; import { ObjectView } from '../ObjectView'; +import { installExplainDouble } from './explainDouble'; const rows = [{ id: '1', name: 'Alice' }]; @@ -93,7 +94,15 @@ async function openSplitPanel() { fireEvent.click(cell); } -afterEach(() => cleanup()); +// objectui#4688 — ObjectGrid batches a record-level explain probe for the +// rows on screen; with no host `apiFetch` here it would otherwise escape to +// the real network under happy-dom. See `explainDouble.ts`. +beforeEach(() => installExplainDouble()); + +afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); +}); describe('ObjectView split-mode detail heading (objectui#3459)', () => { it('renders the object label in English under an en session', async () => { diff --git a/packages/plugin-view/src/__tests__/ObjectView.overlayTitleNoProviderFallback.test.tsx b/packages/plugin-view/src/__tests__/ObjectView.overlayTitleNoProviderFallback.test.tsx index 2d89526f22..8e83b4d8f0 100644 --- a/packages/plugin-view/src/__tests__/ObjectView.overlayTitleNoProviderFallback.test.tsx +++ b/packages/plugin-view/src/__tests__/ObjectView.overlayTitleNoProviderFallback.test.tsx @@ -46,11 +46,12 @@ */ import React from 'react'; -import { describe, it, expect, vi, afterEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, cleanup, fireEvent, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; import type { DataSource } from '@object-ui/types'; import { ObjectView } from '../ObjectView'; +import { installExplainDouble } from './explainDouble'; const rows = [{ id: '1', name: 'Alice' }]; @@ -88,7 +89,15 @@ async function openSplitPanel() { fireEvent.click(cell); } -afterEach(() => cleanup()); +// objectui#4688 — ObjectGrid batches a record-level explain probe for the +// rows on screen; with no host `apiFetch` here it would otherwise escape to +// the real network under happy-dom. See `explainDouble.ts`. +beforeEach(() => installExplainDouble()); + +afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); +}); describe('ObjectView split heading — English fallback with no provider (objectui#3459)', () => { it('interpolates the object label in English, never the raw key', async () => { diff --git a/packages/plugin-view/src/__tests__/explainDouble.ts b/packages/plugin-view/src/__tests__/explainDouble.ts new file mode 100644 index 0000000000..5006e669fb --- /dev/null +++ b/packages/plugin-view/src/__tests__/explainDouble.ts @@ -0,0 +1,76 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * A stand-in for the record-level explain probe, for this package's suites + * that render a real `ObjectGrid` (via `ObjectView`) without wiring a host + * `apiFetch` themselves. + * + * [objectui#4688] `ObjectGrid` batches a record-grained write verdict for the + * rows on screen (`POST /api/v1/security/explain` with `recordIds`), rooted + * in [#4296]. With no host `apiFetch` in the tree the hook falls back to the + * GLOBAL fetch — by design, for standalone embeds — which under happy-dom is + * a REAL request to the default origin. The verdict fails open on a failed + * request, so a suite that ignores it stays green while stderr fills with + * `connect ECONNREFUSED 127.0.0.1:3000`: the same class of escape as + * objectui#3339 (detail side, closed by PR #4105) and the shape that PR + * settled — answer it from a double, never from the network, and never from + * a global error sink. + * + * This is a per-package copy of `packages/plugin-grid/src/__tests__/ + * explainDouble.ts`, following the #4105 convention (no repo convention for + * importing another package's test-only files across a workspace boundary). + * + * Deliberately NOT a swallow-everything stub: it RECORDS every URL it is + * handed and answers only the explain endpoint, so an escape to some other + * endpoint stays observable instead of vanishing into a rejection the hook + * discards. + * + * The answer is `visible: true` for every id asked about, which reproduces + * the pre-#4296 rendering exactly — a permitted record narrows nothing, so no + * assertion in a consuming suite changes meaning. + */ +import { vi } from 'vitest'; + +export interface ExplainDoubleCall { + url: string; + body: Record | undefined; +} + +/** + * Install the double on the global `fetch`. Pair with `vi.unstubAllGlobals()` + * in `afterEach`. + * + * @returns the live call log, in request order. + */ +export function installExplainDouble(): ExplainDoubleCall[] { + const calls: ExplainDoubleCall[] = []; + vi.stubGlobal( + 'fetch', + vi.fn(async (url: unknown, init?: { body?: unknown }) => { + const body = init?.body + ? (JSON.parse(String(init.body)) as Record) + : undefined; + calls.push({ url: String(url), body }); + const recordIds = Array.isArray(body?.recordIds) ? (body!.recordIds as string[]) : undefined; + return { + ok: true, + status: 200, + json: async () => ({ + allowed: true, + object: body?.object, + operation: body?.operation, + principal: { userId: 'u_test' }, + layers: [], + ...(recordIds ? { records: recordIds.map((id) => ({ recordId: id, visible: true })) } : {}), + }), + }; + }), + ); + return calls; +} From 2341cd30984603dbb296df57d36f4570d9b37a2d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 10:38:35 +0000 Subject: [PATCH 2/2] chore: add changeset for plugin-view explain-double test fix --- .changeset/plugin-view-explain-double.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/plugin-view-explain-double.md diff --git a/.changeset/plugin-view-explain-double.md b/.changeset/plugin-view-explain-double.md new file mode 100644 index 0000000000..9d89eb0b41 --- /dev/null +++ b/.changeset/plugin-view-explain-double.md @@ -0,0 +1,7 @@ +--- +--- + +Test-only: install the recorded record-level explain-probe double +(`explainDouble.ts`, per PR #4105's convention) in `plugin-view`'s +`ObjectView` split-mode overlay-title suites, so they no longer escape to the +real network under happy-dom. No published behaviour changes.