Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/network-escape-batch4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
---

Test-only change: the four `app-shell` `console/home` network-escape files in batch 4 of
objectui#7307 now serve their `/api/v1/meta/_drafts` probe from a recording double instead
of a real socket, and their lines leave `KNOWN_ESCAPES` and `PINNED_LEDGER` together. The
double answers an empty draft ledger, which is what `PendingDraftsBanner` already rendered
from the failed read, so no assertion moves. No published behaviour changes — no product
source is touched.
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@
*/

import '@testing-library/jest-dom/vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import React from 'react';
import { render, screen } from '@testing-library/react';
import { render, screen, cleanup } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

const navigateMock = vi.fn();
Expand Down Expand Up @@ -119,6 +119,77 @@ vi.mock('../../../runtime-config', () => ({

import { HomePage } from '../HomePage';

/* ── The `_drafts` double (objectui#7307) ─────────────────────────────────────
* Every render of `HomePage` below mounts `PendingDraftsBanner`, which reads the
* env-wide pending-draft count through `usePendingDrafts({})`. That hook fetches
* `GET /api/v1/meta/_drafts` with the GLOBAL `fetch` — `usePendingDrafts.ts:48`,
* no `apiFetch` seam anywhere on the path — from its mount effect
* (`usePendingDrafts.ts:116` via `refresh` at `:94`). Under happy-dom that global
* is a real HTTP client and the document URL defaults to `http://localhost:3000`,
* so the relative path resolved to a live socket, once per case. The hook's read
* is best-effort (its `catch` leaves `count` at `null`), which is why these cases
* stayed green while the request always failed.
*
* Answered from a RECORDING double — the shape objectui#5225 settled on, carried
* by `packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx` and by
* this burn-down's earlier batches. Deliberately NOT a blanket network stub: it
* records every URL it is handed and `afterEach` fails on any URL outside the set
* it serves, so an escape to somewhere else reds here instead of vanishing into
* that `catch`.
*
* What it answers, and why that changes no assertion here: a known-EMPTY draft
* ledger, in the `{ drafts: [...] }` envelope `fetchPendingDrafts` reads (the one
* `MetadataClient.listDrafts` pins for this endpoint; the bare-array and
* `{ data: { drafts } }` shapes parse to the same rows). Empty rather than seeded
* is load-bearing: `PendingDraftsBanner` renders `null` when `(count ?? 0) <= 0`,
* and the failing request produced `count === null` — so an empty ledger yields
* byte-identical output to what these cases have always rendered, while a seeded
* one would add a banner and a `pending-drafts-publish` button to every case's
* tree. Routes are matched on the PATHNAME because the hook appends a
* `?packageId=` scope for package-bound callers; the full URL is what gets
* recorded.
* ─────────────────────────────────────────────────────────────────────────── */

const DRAFTS_ROUTE = '/api/v1/meta/_drafts';

/** Every URL this file's renders handed the global `fetch`, in request order. */
let draftsCalls: string[] = [];

/** The route key of a recorded URL: its pathname, without the scope query. */
const routeOf = (url: string) => url.split('?')[0];

/** Serve `GET /api/v1/meta/_drafts` as an empty ledger; record everything. */
function installDraftsDouble() {
draftsCalls = [];
vi.stubGlobal(
'fetch',
vi.fn(async (input: unknown) => {
const url = String(
input && typeof input === 'object' && 'url' in input ? (input as { url: unknown }).url : input,
);
draftsCalls.push(url);
if (routeOf(url) !== DRAFTS_ROUTE) return { ok: false, status: 404, json: async () => ({}) };
return { ok: true, status: 200, json: async () => ({ drafts: [] }) };
}),
);
}

beforeEach(installDraftsDouble);

afterEach(() => {
// The double is a router, not a sink: an escape to any OTHER endpoint fails
// here instead of vanishing into the hook's best-effort `catch`.
expect(draftsCalls.filter((url) => routeOf(url) !== DRAFTS_ROUTE)).toEqual([]);
// Unmount BEFORE restoring the real `fetch`. Vitest runs `afterEach` hooks in
// reverse registration order, so this file's teardown runs before the root
// setup's RTL cleanup: unstubbing first would leave the tree mounted with the
// real global back in place, and a mount effect settling in that window
// escapes again (objectui#7439).
cleanup();
vi.unstubAllGlobals();
});


const app = (name: string, extra: Record<string, unknown> = {}) => ({ name, label: name, ...extra });

async function clickApprovals() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@
*/

import '@testing-library/jest-dom/vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import React from 'react';
import { render, screen } from '@testing-library/react';
import { render, screen, cleanup } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions';

Expand Down Expand Up @@ -127,6 +127,77 @@ vi.mock('../../../runtime-config', () => ({

import { HomePage } from '../HomePage';

/* ── The `_drafts` double (objectui#7307) ─────────────────────────────────────
* Every render of `HomePage` below mounts `PendingDraftsBanner`, which reads the
* env-wide pending-draft count through `usePendingDrafts({})`. That hook fetches
* `GET /api/v1/meta/_drafts` with the GLOBAL `fetch` — `usePendingDrafts.ts:48`,
* no `apiFetch` seam anywhere on the path — from its mount effect
* (`usePendingDrafts.ts:116` via `refresh` at `:94`). Under happy-dom that global
* is a real HTTP client and the document URL defaults to `http://localhost:3000`,
* so the relative path resolved to a live socket, once per case. The hook's read
* is best-effort (its `catch` leaves `count` at `null`), which is why these cases
* stayed green while the request always failed.
*
* Answered from a RECORDING double — the shape objectui#5225 settled on, carried
* by `packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx` and by
* this burn-down's earlier batches. Deliberately NOT a blanket network stub: it
* records every URL it is handed and `afterEach` fails on any URL outside the set
* it serves, so an escape to somewhere else reds here instead of vanishing into
* that `catch`.
*
* What it answers, and why that changes no assertion here: a known-EMPTY draft
* ledger, in the `{ drafts: [...] }` envelope `fetchPendingDrafts` reads (the one
* `MetadataClient.listDrafts` pins for this endpoint; the bare-array and
* `{ data: { drafts } }` shapes parse to the same rows). Empty rather than seeded
* is load-bearing: `PendingDraftsBanner` renders `null` when `(count ?? 0) <= 0`,
* and the failing request produced `count === null` — so an empty ledger yields
* byte-identical output to what these cases have always rendered, while a seeded
* one would add a banner and a `pending-drafts-publish` button to every case's
* tree. Routes are matched on the PATHNAME because the hook appends a
* `?packageId=` scope for package-bound callers; the full URL is what gets
* recorded.
* ─────────────────────────────────────────────────────────────────────────── */

const DRAFTS_ROUTE = '/api/v1/meta/_drafts';

/** Every URL this file's renders handed the global `fetch`, in request order. */
let draftsCalls: string[] = [];

/** The route key of a recorded URL: its pathname, without the scope query. */
const routeOf = (url: string) => url.split('?')[0];

/** Serve `GET /api/v1/meta/_drafts` as an empty ledger; record everything. */
function installDraftsDouble() {
draftsCalls = [];
vi.stubGlobal(
'fetch',
vi.fn(async (input: unknown) => {
const url = String(
input && typeof input === 'object' && 'url' in input ? (input as { url: unknown }).url : input,
);
draftsCalls.push(url);
if (routeOf(url) !== DRAFTS_ROUTE) return { ok: false, status: 404, json: async () => ({}) };
return { ok: true, status: 200, json: async () => ({ drafts: [] }) };
}),
);
}

beforeEach(installDraftsDouble);

afterEach(() => {
// The double is a router, not a sink: an escape to any OTHER endpoint fails
// here instead of vanishing into the hook's best-effort `catch`.
expect(draftsCalls.filter((url) => routeOf(url) !== DRAFTS_ROUTE)).toEqual([]);
// Unmount BEFORE restoring the real `fetch`. Vitest runs `afterEach` hooks in
// reverse registration order, so this file's teardown runs before the root
// setup's RTL cleanup: unstubbing first would leave the tree mounted with the
// real global back in place, and a mount effect settling in that window
// escapes again (objectui#7439).
cleanup();
vi.unstubAllGlobals();
});


/**
* The `/me/permissions` answer recorded on objectstack#8270 for the EE
* workspace owner. `systemPermissions` is present and NON-empty — it simply
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@
*/

import '@testing-library/jest-dom/vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import React from 'react';
import { render, screen } from '@testing-library/react';
import { render, screen, cleanup } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

const navigateMock = vi.fn();
Expand Down Expand Up @@ -121,6 +121,77 @@ vi.mock('../../../runtime-config', () => ({

import { HomePage } from '../HomePage';

/* ── The `_drafts` double (objectui#7307) ─────────────────────────────────────
* Every render of `HomePage` below mounts `PendingDraftsBanner`, which reads the
* env-wide pending-draft count through `usePendingDrafts({})`. That hook fetches
* `GET /api/v1/meta/_drafts` with the GLOBAL `fetch` — `usePendingDrafts.ts:48`,
* no `apiFetch` seam anywhere on the path — from its mount effect
* (`usePendingDrafts.ts:116` via `refresh` at `:94`). Under happy-dom that global
* is a real HTTP client and the document URL defaults to `http://localhost:3000`,
* so the relative path resolved to a live socket, once per case. The hook's read
* is best-effort (its `catch` leaves `count` at `null`), which is why these cases
* stayed green while the request always failed.
*
* Answered from a RECORDING double — the shape objectui#5225 settled on, carried
* by `packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx` and by
* this burn-down's earlier batches. Deliberately NOT a blanket network stub: it
* records every URL it is handed and `afterEach` fails on any URL outside the set
* it serves, so an escape to somewhere else reds here instead of vanishing into
* that `catch`.
*
* What it answers, and why that changes no assertion here: a known-EMPTY draft
* ledger, in the `{ drafts: [...] }` envelope `fetchPendingDrafts` reads (the one
* `MetadataClient.listDrafts` pins for this endpoint; the bare-array and
* `{ data: { drafts } }` shapes parse to the same rows). Empty rather than seeded
* is load-bearing: `PendingDraftsBanner` renders `null` when `(count ?? 0) <= 0`,
* and the failing request produced `count === null` — so an empty ledger yields
* byte-identical output to what these cases have always rendered, while a seeded
* one would add a banner and a `pending-drafts-publish` button to every case's
* tree. Routes are matched on the PATHNAME because the hook appends a
* `?packageId=` scope for package-bound callers; the full URL is what gets
* recorded.
* ─────────────────────────────────────────────────────────────────────────── */

const DRAFTS_ROUTE = '/api/v1/meta/_drafts';

/** Every URL this file's renders handed the global `fetch`, in request order. */
let draftsCalls: string[] = [];

/** The route key of a recorded URL: its pathname, without the scope query. */
const routeOf = (url: string) => url.split('?')[0];

/** Serve `GET /api/v1/meta/_drafts` as an empty ledger; record everything. */
function installDraftsDouble() {
draftsCalls = [];
vi.stubGlobal(
'fetch',
vi.fn(async (input: unknown) => {
const url = String(
input && typeof input === 'object' && 'url' in input ? (input as { url: unknown }).url : input,
);
draftsCalls.push(url);
if (routeOf(url) !== DRAFTS_ROUTE) return { ok: false, status: 404, json: async () => ({}) };
return { ok: true, status: 200, json: async () => ({ drafts: [] }) };
}),
);
}

beforeEach(installDraftsDouble);

afterEach(() => {
// The double is a router, not a sink: an escape to any OTHER endpoint fails
// here instead of vanishing into the hook's best-effort `catch`.
expect(draftsCalls.filter((url) => routeOf(url) !== DRAFTS_ROUTE)).toEqual([]);
// Unmount BEFORE restoring the real `fetch`. Vitest runs `afterEach` hooks in
// reverse registration order, so this file's teardown runs before the root
// setup's RTL cleanup: unstubbing first would leave the tree mounted with the
// real global back in place, and a mount effect settling in that window
// escapes again (objectui#7439).
cleanup();
vi.unstubAllGlobals();
});


const app = (name: string, extra: Record<string, unknown> = {}) => ({ name, label: name, ...extra });

async function clickAndReadTarget(open: (user: ReturnType<typeof userEvent.setup>) => Promise<void>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@
*/

import '@testing-library/jest-dom/vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import React from 'react';
import { render, screen } from '@testing-library/react';
import { render, screen, cleanup } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

const navigateMock = vi.fn();
Expand Down Expand Up @@ -117,6 +117,77 @@ vi.mock('../../../runtime-config', () => ({

import { HomePage } from '../HomePage';

/* ── The `_drafts` double (objectui#7307) ─────────────────────────────────────
* Every render of `HomePage` below mounts `PendingDraftsBanner`, which reads the
* env-wide pending-draft count through `usePendingDrafts({})`. That hook fetches
* `GET /api/v1/meta/_drafts` with the GLOBAL `fetch` — `usePendingDrafts.ts:48`,
* no `apiFetch` seam anywhere on the path — from its mount effect
* (`usePendingDrafts.ts:116` via `refresh` at `:94`). Under happy-dom that global
* is a real HTTP client and the document URL defaults to `http://localhost:3000`,
* so the relative path resolved to a live socket, once per case. The hook's read
* is best-effort (its `catch` leaves `count` at `null`), which is why these cases
* stayed green while the request always failed.
*
* Answered from a RECORDING double — the shape objectui#5225 settled on, carried
* by `packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx` and by
* this burn-down's earlier batches. Deliberately NOT a blanket network stub: it
* records every URL it is handed and `afterEach` fails on any URL outside the set
* it serves, so an escape to somewhere else reds here instead of vanishing into
* that `catch`.
*
* What it answers, and why that changes no assertion here: a known-EMPTY draft
* ledger, in the `{ drafts: [...] }` envelope `fetchPendingDrafts` reads (the one
* `MetadataClient.listDrafts` pins for this endpoint; the bare-array and
* `{ data: { drafts } }` shapes parse to the same rows). Empty rather than seeded
* is load-bearing: `PendingDraftsBanner` renders `null` when `(count ?? 0) <= 0`,
* and the failing request produced `count === null` — so an empty ledger yields
* byte-identical output to what these cases have always rendered, while a seeded
* one would add a banner and a `pending-drafts-publish` button to every case's
* tree. Routes are matched on the PATHNAME because the hook appends a
* `?packageId=` scope for package-bound callers; the full URL is what gets
* recorded.
* ─────────────────────────────────────────────────────────────────────────── */

const DRAFTS_ROUTE = '/api/v1/meta/_drafts';

/** Every URL this file's renders handed the global `fetch`, in request order. */
let draftsCalls: string[] = [];

/** The route key of a recorded URL: its pathname, without the scope query. */
const routeOf = (url: string) => url.split('?')[0];

/** Serve `GET /api/v1/meta/_drafts` as an empty ledger; record everything. */
function installDraftsDouble() {
draftsCalls = [];
vi.stubGlobal(
'fetch',
vi.fn(async (input: unknown) => {
const url = String(
input && typeof input === 'object' && 'url' in input ? (input as { url: unknown }).url : input,
);
draftsCalls.push(url);
if (routeOf(url) !== DRAFTS_ROUTE) return { ok: false, status: 404, json: async () => ({}) };
return { ok: true, status: 200, json: async () => ({ drafts: [] }) };
}),
);
}

beforeEach(installDraftsDouble);

afterEach(() => {
// The double is a router, not a sink: an escape to any OTHER endpoint fails
// here instead of vanishing into the hook's best-effort `catch`.
expect(draftsCalls.filter((url) => routeOf(url) !== DRAFTS_ROUTE)).toEqual([]);
// Unmount BEFORE restoring the real `fetch`. Vitest runs `afterEach` hooks in
// reverse registration order, so this file's teardown runs before the root
// setup's RTL cleanup: unstubbing first would leave the tree mounted with the
// real global back in place, and a mount effect settling in that window
// escapes again (objectui#7439).
cleanup();
vi.unstubAllGlobals();
});


const app = (name: string, extra: Record<string, unknown> = {}) => ({ name, label: name, ...extra });

/** Click the seeded notification row and read the argument `navigate()` got. */
Expand Down
4 changes: 0 additions & 4 deletions scripts/__tests__/network-escape-ledger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,6 @@ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../
* and must be done in lockstep with `KNOWN_ESCAPES`.
*/
const PINNED_LEDGER: readonly string[] = [
'packages/app-shell/src/console/home/__tests__/HomePage.approvalsTarget.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.inboxLinksTarget.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.notificationDeepLink.test.tsx',
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.inactiveRetained.test.tsx',
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.specKeys.test.tsx',
'packages/app-shell/src/views/studio-design/StudioDesignSurface.designerRegistryMissing.test.tsx',
Expand Down
Loading
Loading