|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * objectui#12233 — a lookup on a custom page carries the router's basename, |
| 5 | + * as the record page's lookups do. |
| 6 | + * |
| 7 | + * `PageView` publishes the console's record navigator to the blocks on the |
| 8 | + * page (`pageRecordActionsValue`, objectui#11293), and a lookup there renders |
| 9 | + * a real anchor from its `recordHref`. That builder returned the bare route |
| 10 | + * `/apps/APP/OBJECT/record/ID`, so on a console mounted at `/_console` a new |
| 11 | + * tab, a middle-click or a copied link left the console, while a left click |
| 12 | + * (routed by `openRecord`) worked. |
| 13 | + * |
| 14 | + * The page, the router and the lookup are real: `PageView` under a |
| 15 | + * `MemoryRouter`, and the real `LookupCellRenderer` reading the context the |
| 16 | + * page publishes. `SchemaRenderer` is replaced by that one lookup, the stand-in |
| 17 | + * the objectui#11293 pin beside this file uses for "a block on the page". The |
| 18 | + * oracle for the href is react-router's own `Link` for the same route. The |
| 19 | + * control mounts the same tree with no basename and keeps the bare href. |
| 20 | + */ |
| 21 | + |
| 22 | +import * as React from 'react'; |
| 23 | +import { describe, it, expect, vi, afterEach } from 'vitest'; |
| 24 | +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; |
| 25 | +import { Link, MemoryRouter, Route, Routes, useLocation } from 'react-router-dom'; |
| 26 | +import '@testing-library/jest-dom'; |
| 27 | +import { LookupCellRenderer } from '@object-ui/fields'; |
| 28 | + |
| 29 | +const PAGE_NAME = 'the_page'; |
| 30 | +const APP = 'crm'; |
| 31 | +const ACCOUNT = { id: 'A-1', name: 'Acme Corp' }; |
| 32 | +const ACCOUNT_ROUTE = `/apps/${APP}/account/record/${ACCOUNT.id}`; |
| 33 | + |
| 34 | +vi.mock('@object-ui/auth', async (importOriginal) => ({ |
| 35 | + ...(await importOriginal<Record<string, unknown>>()), |
| 36 | + useAuth: () => ({ |
| 37 | + user: { id: 'u1', name: 'User', role: 'user', image: null }, |
| 38 | + activeOrganization: null, |
| 39 | + }), |
| 40 | + useWorkspaceAdminStatus: () => ({ isAdmin: false, isResolved: true }), |
| 41 | + createAuthenticatedFetch: () => vi.fn(), |
| 42 | +})); |
| 43 | + |
| 44 | +vi.mock('@object-ui/i18n', async (importOriginal) => ({ |
| 45 | + ...(await importOriginal<Record<string, unknown>>()), |
| 46 | + useObjectTranslation: () => ({ |
| 47 | + t: (k: string, o?: { defaultValue?: string; name?: string }) => o?.defaultValue ?? o?.name ?? k, |
| 48 | + }), |
| 49 | +})); |
| 50 | + |
| 51 | +vi.mock('../../providers/MetadataProvider', () => ({ |
| 52 | + useMetadata: () => ({ |
| 53 | + pages: [{ name: 'the_page', label: 'A Page', type: 'app' }], |
| 54 | + objects: [{ name: 'account' }], |
| 55 | + }), |
| 56 | +})); |
| 57 | + |
| 58 | +vi.mock('../MetadataInspector', () => ({ |
| 59 | + MetadataPanel: () => null, |
| 60 | + useMetadataInspector: () => ({ showDebug: false }), |
| 61 | +})); |
| 62 | + |
| 63 | +vi.mock('@object-ui/react', async (orig) => { |
| 64 | + const actual = await orig<typeof import('@object-ui/react')>(); |
| 65 | + return { |
| 66 | + ...actual, |
| 67 | + useAdapter: () => ({}), |
| 68 | + // "A block on the page": one real lookup, reading the context PageView |
| 69 | + // publishes. |
| 70 | + SchemaRenderer: () => ( |
| 71 | + <LookupCellRenderer |
| 72 | + value={{ id: 'A-1', name: 'Acme Corp' }} |
| 73 | + field={{ type: 'lookup', reference: 'account' } as never} |
| 74 | + /> |
| 75 | + ), |
| 76 | + }; |
| 77 | +}); |
| 78 | + |
| 79 | +import { PageView } from '../PageView'; |
| 80 | + |
| 81 | +afterEach(() => { |
| 82 | + cleanup(); |
| 83 | +}); |
| 84 | + |
| 85 | +/** The router's own location, basename stripped: where a left click took us. */ |
| 86 | +function RouterLocation() { |
| 87 | + const location = useLocation(); |
| 88 | + return <output data-testid="router-location">{`${location.pathname}${location.search}`}</output>; |
| 89 | +} |
| 90 | + |
| 91 | +function mount(basename?: string) { |
| 92 | + const root = basename ?? ''; |
| 93 | + return render( |
| 94 | + <MemoryRouter basename={basename} initialEntries={[`${root}/apps/${APP}/page/${PAGE_NAME}`]}> |
| 95 | + <Routes> |
| 96 | + <Route path="/apps/:appName/page/:pageName" element={<PageView />} /> |
| 97 | + <Route path="*" element={null} /> |
| 98 | + </Routes> |
| 99 | + <Link to={ACCOUNT_ROUTE} data-testid="oracle"> |
| 100 | + oracle |
| 101 | + </Link> |
| 102 | + <RouterLocation /> |
| 103 | + </MemoryRouter>, |
| 104 | + ); |
| 105 | +} |
| 106 | + |
| 107 | +const accountAnchor = async () => (await screen.findByText(ACCOUNT.name)).closest('a'); |
| 108 | + |
| 109 | +describe('a lookup on a custom page carries the router basename (objectui#12233)', () => { |
| 110 | + it('under basename /_console: the href is the one react-router renders for the route, and a left click routes there once', async () => { |
| 111 | + mount('/_console'); |
| 112 | + const anchor = await accountAnchor(); |
| 113 | + const href = anchor?.getAttribute('href') ?? null; |
| 114 | + expect(href).toBe(`/_console${ACCOUNT_ROUTE}`); |
| 115 | + expect(href).toBe(screen.getByTestId('oracle').getAttribute('href')); |
| 116 | + fireEvent.click(anchor as Element, { button: 0 }); |
| 117 | + expect(screen.getByTestId('router-location').textContent).toBe(ACCOUNT_ROUTE); |
| 118 | + }); |
| 119 | + |
| 120 | + it('control, no basename: the href is the bare route, as before', async () => { |
| 121 | + mount(); |
| 122 | + const anchor = await accountAnchor(); |
| 123 | + expect(anchor?.getAttribute('href')).toBe(ACCOUNT_ROUTE); |
| 124 | + expect(anchor?.getAttribute('href')).toBe(screen.getByTestId('oracle').getAttribute('href')); |
| 125 | + fireEvent.click(anchor as Element, { button: 0 }); |
| 126 | + expect(screen.getByTestId('router-location').textContent).toBe(ACCOUNT_ROUTE); |
| 127 | + }); |
| 128 | +}); |
0 commit comments