From 2b8614523af72fe5103be2e16155ad011730eda4 Mon Sep 17 00:00:00 2001 From: Sundram Gupta Date: Thu, 23 Jul 2026 01:31:09 +0530 Subject: [PATCH 1/3] feat(oc-docs): resolve variables in playground example and request URLs Render the example view URL through VariableText so {{variables}} show the docs-style highlighted pill and hover tooltip (name, scope, resolved value). Replace the playground request URL with HighlightedInput wired to the variable resolver, so the editable URL paints variable tokens in the same colour scheme, shows the variable info card on hover, and offers {{variable}} autocomplete. Add an optional onEnter prop to HighlightedInput to preserve Enter-to-send, guarded against IME composition. --- .../HighlightedInput/HighlightedInput.tsx | 12 +++++-- .../Content/Views/ExampleView/ExampleView.tsx | 3 +- .../PlaygroundView/QueryBar/QueryBar.tsx | 15 +++++--- .../PlaygroundView/QueryBar/StyledWrapper.ts | 35 +++++++++++++------ 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/packages/oc-docs/src/components/HighlightedInput/HighlightedInput.tsx b/packages/oc-docs/src/components/HighlightedInput/HighlightedInput.tsx index 08282f13..76b363ca 100644 --- a/packages/oc-docs/src/components/HighlightedInput/HighlightedInput.tsx +++ b/packages/oc-docs/src/components/HighlightedInput/HighlightedInput.tsx @@ -25,6 +25,7 @@ interface HighlightedInputProps { title?: string; testId?: string; multiline?: boolean; + onEnter?: () => void; } interface HoveredToken { @@ -75,7 +76,8 @@ export const HighlightedInput: React.FC = ({ variablesAutocomplete = true, title, testId, - multiline = false + multiline = false, + onEnter }) => { const inputRef = useRef(null); const mirrorRef = useRef(null); @@ -253,7 +255,13 @@ export const HighlightedInput: React.FC = ({ }; const handleKeyDown = (event: React.KeyboardEvent) => { - if (!autocomplete) return; + if (!autocomplete) { + if (event.key === 'Enter' && !multiline && onEnter && !event.nativeEvent.isComposing) { + event.preventDefault(); + onEnter(); + } + return; + } const { items, active } = autocomplete; switch (event.key) { case 'ArrowDown': diff --git a/packages/oc-docs/src/components/Playground/Content/Views/ExampleView/ExampleView.tsx b/packages/oc-docs/src/components/Playground/Content/Views/ExampleView/ExampleView.tsx index b30cf220..c61ec7fa 100644 --- a/packages/oc-docs/src/components/Playground/Content/Views/ExampleView/ExampleView.tsx +++ b/packages/oc-docs/src/components/Playground/Content/Views/ExampleView/ExampleView.tsx @@ -6,6 +6,7 @@ import { PropertyTable } from '../../../../PropertyTable/PropertyTable'; import { RequestParams } from '../../../../Request/RequestParams/RequestParams'; import { RequestBody } from '../../../../Request/RequestBody/RequestBody'; import { Code } from '../../../../Code/Code'; +import { VariableText } from '../../../../VariableText/VariableText'; import { SplitDivider } from '../../../../SplitDivider/SplitDivider'; import { resolvePathAndQueryParams } from '../../../../../utils/pathParams'; import { getBodyView, getDescription, headerRows } from '../../../../../utils/request'; @@ -74,7 +75,7 @@ export const ExampleView: React.FC = ({ request, example, orie
- {url} + {statusBadge}
diff --git a/packages/oc-docs/src/components/Playground/Content/Views/PlaygroundView/QueryBar/QueryBar.tsx b/packages/oc-docs/src/components/Playground/Content/Views/PlaygroundView/QueryBar/QueryBar.tsx index 3226b248..b9269e4a 100644 --- a/packages/oc-docs/src/components/Playground/Content/Views/PlaygroundView/QueryBar/QueryBar.tsx +++ b/packages/oc-docs/src/components/Playground/Content/Views/PlaygroundView/QueryBar/QueryBar.tsx @@ -7,6 +7,8 @@ import { syncPathParams, syncQueryParams } from '../../../../../../utils/pathPar import { availableMethods, getMethodColorVar } from '../../../../../../theme/methodColors'; import { MethodBadge } from '../../../../../MethodBadge/MethodBadge'; import { CopyButton } from '../../../../../../ui/CopyButton/CopyButton'; +import { HighlightedInput } from '../../../../../HighlightedInput/HighlightedInput'; +import { useResolvedVariables } from '../../../../../../hooks'; import { SendIcon } from '../../../../../../assets/icons'; interface QueryBarProps { @@ -17,6 +19,7 @@ interface QueryBarProps { } const QueryBar: React.FC = ({ item, onSendRequest, isLoading, onItemChange }) => { + const { isFound, names } = useResolvedVariables(); const [url, setUrl] = useState(getRequestUrl(item)); const [method, setMethod] = useState(getHttpMethod(item)); @@ -75,13 +78,15 @@ const QueryBar: React.FC = ({ item, onSendRequest, isLoading, onI - handleUrlChange(e.target.value)} + onValueChange={handleUrlChange} placeholder="Enter request URL" - onKeyPress={(e) => { - if (e.key === 'Enter' && url.trim() && !isLoading) { + isFound={isFound} + names={names} + testId="query-bar-url" + onEnter={() => { + if (url.trim() && !isLoading) { onSendRequest(); } }} diff --git a/packages/oc-docs/src/components/Playground/Content/Views/PlaygroundView/QueryBar/StyledWrapper.ts b/packages/oc-docs/src/components/Playground/Content/Views/PlaygroundView/QueryBar/StyledWrapper.ts index e3fa8bc5..9fe90bf3 100644 --- a/packages/oc-docs/src/components/Playground/Content/Views/PlaygroundView/QueryBar/StyledWrapper.ts +++ b/packages/oc-docs/src/components/Playground/Content/Views/PlaygroundView/QueryBar/StyledWrapper.ts @@ -9,23 +9,38 @@ export const StyledWrapper = styled.div` border-radius: var(--oc-radius); background-color: var(--bg-primary); - input { + && .highlight-input { flex: 1; min-width: 0; - outline: none; - border: none; - border-radius: 0; - background-color: transparent; + padding: 0; font-family: var(--font-mono); - font-weight: 400; + } + + && .highlight-input .text-input, + && .highlight-input .highlight-input-mirror { + padding: 0; font-size: 0.75rem; line-height: 1.125rem; + } + + && .highlight-input .highlight-input-mirror { + right: 0; + left: 0; color: var(--text-primary); + } - &::placeholder { - color: var(--text-secondary); - opacity: 0.6; - } + && .highlight-input .highlight-input-mirror .variable-valid, + && .highlight-input .highlight-input-mirror .variable-invalid { + color: var(--primary-text); + background-color: var(--brand-soft); + border-radius: 0.1875rem; + padding: 0 0.1875rem; + margin: 0 -0.1875rem; + } + + && .highlight-input .text-input::placeholder { + color: var(--text-secondary); + opacity: 0.6; } .method-select-wrapper { From 884ddd7d1bf02128c05dddf1b961f7056e68f437 Mon Sep 17 00:00:00 2001 From: Sundram Gupta Date: Thu, 23 Jul 2026 02:33:06 +0530 Subject: [PATCH 2/3] test(oc-docs): e2e for variable resolution in playground URLs Cover the example view URL variable hover card (name, scope, resolved value) and the request URL bar's variable rendering plus {{variable}} autocomplete. Adds a QueryBar component object and wires the variable card and playground environment switcher onto PlaygroundComponent. --- .../e2e/components/playground.component.ts | 6 +++ .../playground/query-bar.component.ts | 17 ++++++++ .../playground-variable-resolution.spec.ts | 40 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 packages/oc-docs/e2e/components/playground/query-bar.component.ts create mode 100644 packages/oc-docs/e2e/tests/playground/playground-variable-resolution.spec.ts diff --git a/packages/oc-docs/e2e/components/playground.component.ts b/packages/oc-docs/e2e/components/playground.component.ts index ea549374..2594f989 100644 --- a/packages/oc-docs/e2e/components/playground.component.ts +++ b/packages/oc-docs/e2e/components/playground.component.ts @@ -2,12 +2,18 @@ import type { Locator } from '@playwright/test'; import { BaseComponent } from './base.component'; import { KeyValueTableComponent } from './key-value-table/key-value-table.component'; import { CodeEditorComponent } from './code-editor/code-editor.component'; +import { QueryBarComponent } from './playground/query-bar.component'; +import { VariableCardComponent } from './variable-card/variable-card.component'; +import { EnvSwitcherComponent } from './layout/env-switcher.component'; import type { DockMode } from '../../src/utils/playgroundDock'; export class PlaygroundComponent extends BaseComponent { readonly keyValueTable = new KeyValueTableComponent(this.page); readonly preRequestScriptEditor = new CodeEditorComponent(this.page, 'scripts-editor-pre-request'); readonly postResponseScriptEditor = new CodeEditorComponent(this.page, 'scripts-editor-post-response'); + readonly queryBar = new QueryBarComponent(this.page); + readonly variableCard = new VariableCardComponent(this.page); + readonly environmentSwitcher = new EnvSwitcherComponent(this.page, 'playground-env-switcher'); readonly header = this.page.getByTestId('playground-header'); readonly switcher = this.page.getByTestId('playground-dock-switcher'); diff --git a/packages/oc-docs/e2e/components/playground/query-bar.component.ts b/packages/oc-docs/e2e/components/playground/query-bar.component.ts new file mode 100644 index 00000000..4b3104e7 --- /dev/null +++ b/packages/oc-docs/e2e/components/playground/query-bar.component.ts @@ -0,0 +1,17 @@ +import type { Locator } from '@playwright/test'; +import { BaseComponent } from '../base.component'; + +/** + * The playground request URL bar. The URL is a HighlightedInput whose real + * carries the test id; the {{variable}} autocomplete list is portaled + * to under its own test id, so both are located page-wide. + */ +export class QueryBarComponent extends BaseComponent { + readonly url = this.page.getByTestId('query-bar-url'); + readonly copyButton = this.page.getByTestId('query-bar-copy-url'); + readonly autocomplete = this.page.getByTestId('variable-autocomplete'); + + option(name: string): Locator { + return this.autocomplete.getByRole('option', { name }); + } +} diff --git a/packages/oc-docs/e2e/tests/playground/playground-variable-resolution.spec.ts b/packages/oc-docs/e2e/tests/playground/playground-variable-resolution.spec.ts new file mode 100644 index 00000000..cab1ce88 --- /dev/null +++ b/packages/oc-docs/e2e/tests/playground/playground-variable-resolution.spec.ts @@ -0,0 +1,40 @@ +import { test, expect } from '../../playwright'; + +const OPEN = '/#/?pg=1&dock=bottom'; +const REQUEST = 'get users'; +const EXAMPLE = 'List Users'; + +test.describe('Playground - variable resolution in URLs', () => { + test.beforeEach(async ({ page, playground }) => { + await page.goto(OPEN); + await playground.environmentSwitcher.selectEnvironment('Local'); + }); + + test('example view URL resolves its variable with a hover card', async ({ playground }) => { + await playground.exampleToggle(REQUEST).click(); + await playground.exampleRow(EXAMPLE).click(); + await expect(playground.exampleView).toBeVisible(); + + const { variableCard } = playground; + await variableCard.hoverToken('host'); + + await expect(variableCard.card).toBeVisible(); + await expect(variableCard.name).toHaveText('host'); + await expect(variableCard.scopeBadge).toHaveText('Environment'); + await expect(variableCard.value).toHaveText('http://localhost:8081'); + }); + + test('request URL bar shows the variable and offers variable autocomplete', async ({ playground }) => { + await playground.openSidebarItem(REQUEST); + + const { queryBar } = playground; + await expect(queryBar.url).toBeVisible(); + await expect(queryBar.url).toHaveValue(/\{\{host\}\}/); + + await queryBar.url.fill(''); + await queryBar.url.pressSequentially('{{h'); + + await expect(queryBar.autocomplete).toBeVisible(); + await expect(queryBar.option('host')).toBeVisible(); + }); +}); From ae458e992f4e1be9dd0a5b57669ff9d87d3a0efa Mon Sep 17 00:00:00 2001 From: Sundram Gupta Date: Thu, 23 Jul 2026 14:12:45 +0530 Subject: [PATCH 3/3] fix(oc-docs): color playground URL variables by validity, cover scoped vars Drop the brand-tinted pill override on the playground request URL bar so its {{variable}} tokens use the same valid (green) / invalid (red) colours as the params and headers cells, instead of a flat brand colour that hid whether a variable resolves. Add e2e covering that request-scoped and folder-scoped variables referenced in the playground resolve as valid (not painted red as undefined), guarding the scope the ItemVariableResolverProvider must include. --- .../playground/query-bar.component.ts | 14 ++++++++++ .../playground-variable-resolution.spec.ts | 26 +++++++++++++++++++ .../PlaygroundView/QueryBar/StyledWrapper.ts | 9 ------- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/packages/oc-docs/e2e/components/playground/query-bar.component.ts b/packages/oc-docs/e2e/components/playground/query-bar.component.ts index 4b3104e7..53ec2db7 100644 --- a/packages/oc-docs/e2e/components/playground/query-bar.component.ts +++ b/packages/oc-docs/e2e/components/playground/query-bar.component.ts @@ -5,6 +5,12 @@ import { BaseComponent } from '../base.component'; * The playground request URL bar. The URL is a HighlightedInput whose real * carries the test id; the {{variable}} autocomplete list is portaled * to under its own test id, so both are located page-wide. + * + * The highlight mirror that paints the tokens is aria-hidden and its per-token + * spans carry no test id (they are generated), so token validity is read from + * the .variable-valid / .variable-invalid classes the component assigns - the + * same classes the app hit-tests for hover. A valid token is resolvable (green); + * an invalid one is not (red). */ export class QueryBarComponent extends BaseComponent { readonly url = this.page.getByTestId('query-bar-url'); @@ -14,4 +20,12 @@ export class QueryBarComponent extends BaseComponent { option(name: string): Locator { return this.autocomplete.getByRole('option', { name }); } + + validToken(name: string): Locator { + return this.page.locator('.highlight-input-mirror .variable-valid').filter({ hasText: `{{${name}}}` }); + } + + invalidToken(name: string): Locator { + return this.page.locator('.highlight-input-mirror .variable-invalid').filter({ hasText: `{{${name}}}` }); + } } diff --git a/packages/oc-docs/e2e/tests/playground/playground-variable-resolution.spec.ts b/packages/oc-docs/e2e/tests/playground/playground-variable-resolution.spec.ts index cab1ce88..7f8568c4 100644 --- a/packages/oc-docs/e2e/tests/playground/playground-variable-resolution.spec.ts +++ b/packages/oc-docs/e2e/tests/playground/playground-variable-resolution.spec.ts @@ -38,3 +38,29 @@ test.describe('Playground - variable resolution in URLs', () => { await expect(queryBar.option('host')).toBeVisible(); }); }); + +// Regression for a QA-reported bug: request-scoped and folder-scoped variables +// referenced in the playground were painted red (treated as undefined) even +// though they are defined, because the resolver in scope must include the open +// request and its ancestor folders, not just collection + environment. +test.describe('Playground - request/folder scoped variables resolve', () => { + test.beforeEach(async ({ page, playground }) => { + await page.goto('/?fixture=vars#/?pg=1&dock=bottom'); + await playground.environmentSwitcher.selectEnvironment('Dev'); + await playground.openTreeItem(['Customers', 'Variables Demo']); + await expect(playground.queryBar.url).toBeVisible(); + }); + + test('a request-scoped variable in the URL is valid, not undefined', async ({ playground }) => { + // userId is defined in the request runtime variables (req-42). + await expect(playground.queryBar.validToken('userId')).toHaveCount(1); + await expect(playground.queryBar.invalidToken('userId')).toHaveCount(0); + }); + + test('a folder-scoped variable in a header is valid, not undefined', async ({ playground }) => { + // folderScope is defined on the parent Customers folder (from-folder). + await playground.selectTab('headers'); + await expect(playground.queryBar.validToken('folderScope')).toHaveCount(1); + await expect(playground.queryBar.invalidToken('folderScope')).toHaveCount(0); + }); +}); diff --git a/packages/oc-docs/src/components/Playground/Content/Views/PlaygroundView/QueryBar/StyledWrapper.ts b/packages/oc-docs/src/components/Playground/Content/Views/PlaygroundView/QueryBar/StyledWrapper.ts index 9fe90bf3..ae92fea7 100644 --- a/packages/oc-docs/src/components/Playground/Content/Views/PlaygroundView/QueryBar/StyledWrapper.ts +++ b/packages/oc-docs/src/components/Playground/Content/Views/PlaygroundView/QueryBar/StyledWrapper.ts @@ -29,15 +29,6 @@ export const StyledWrapper = styled.div` color: var(--text-primary); } - && .highlight-input .highlight-input-mirror .variable-valid, - && .highlight-input .highlight-input-mirror .variable-invalid { - color: var(--primary-text); - background-color: var(--brand-soft); - border-radius: 0.1875rem; - padding: 0 0.1875rem; - margin: 0 -0.1875rem; - } - && .highlight-input .text-input::placeholder { color: var(--text-secondary); opacity: 0.6;