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..53ec2db7 --- /dev/null +++ b/packages/oc-docs/e2e/components/playground/query-bar.component.ts @@ -0,0 +1,31 @@ +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. + * + * 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'); + 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 }); + } + + 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 new file mode 100644 index 00000000..7f8568c4 --- /dev/null +++ b/packages/oc-docs/e2e/tests/playground/playground-variable-resolution.spec.ts @@ -0,0 +1,66 @@ +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(); + }); +}); + +// 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/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