Skip to content
Closed
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
6 changes: 6 additions & 0 deletions packages/oc-docs/e2e/components/playground.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
31 changes: 31 additions & 0 deletions packages/oc-docs/e2e/components/playground/query-bar.component.ts
Original file line number Diff line number Diff line change
@@ -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
* <input> carries the test id; the {{variable}} autocomplete list is portaled
* to <body> 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}}}` });
}
}
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface HighlightedInputProps {
title?: string;
testId?: string;
multiline?: boolean;
onEnter?: () => void;
}

interface HoveredToken {
Expand Down Expand Up @@ -75,7 +76,8 @@ export const HighlightedInput: React.FC<HighlightedInputProps> = ({
variablesAutocomplete = true,
title,
testId,
multiline = false
multiline = false,
onEnter
}) => {
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);
const mirrorRef = useRef<HTMLDivElement | null>(null);
Expand Down Expand Up @@ -253,7 +255,13 @@ export const HighlightedInput: React.FC<HighlightedInputProps> = ({
};

const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
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':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -74,7 +75,7 @@ export const ExampleView: React.FC<ExampleViewProps> = ({ request, example, orie

<div className="example-view-urlbar">
<MethodBadge method={method} />
<span className="example-view-url">{url}</span>
<VariableText className="example-view-url" value={url} />
<CopyButton text={url} />
{statusBadge}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -17,6 +19,7 @@ interface QueryBarProps {
}

const QueryBar: React.FC<QueryBarProps> = ({ item, onSendRequest, isLoading, onItemChange }) => {
const { isFound, names } = useResolvedVariables();
const [url, setUrl] = useState(getRequestUrl(item));
const [method, setMethod] = useState(getHttpMethod(item));

Expand Down Expand Up @@ -75,13 +78,15 @@ const QueryBar: React.FC<QueryBarProps> = ({ item, onSendRequest, isLoading, onI
</MenuDropdown>
</div>

<input
type="text"
<HighlightedInput
value={url}
onChange={(e) => 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();
}
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,29 @@ 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 .text-input::placeholder {
color: var(--text-secondary);
opacity: 0.6;
}

.method-select-wrapper {
Expand Down
Loading