Skip to content
Draft
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
2 changes: 1 addition & 1 deletion src/Frontend/src/components/CodeEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const extensions = computed(() => {
</script>

<template>
<div class="wrapper" :aria-label="ariaLabel">
<div class="wrapper" role="code" :aria-label="ariaLabel">
<div v-if="props.showCopyToClipboard || $slots.toolbarLeft || $slots.toolbarRight" class="toolbar">
<div><slot name="toolbarLeft"></slot></div>
<div>
Expand Down
2 changes: 1 addition & 1 deletion src/Frontend/src/components/CopyToClipboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ watch(timeoutId, (_, previousTimeoutId) => clearTimeout(previousTimeoutId));

<template>
<Tippy content="Copied" ref="tippyRef" trigger="manual">
<ActionButton v-if="!props.isIconOnly" variant="secondary" size="sm" :icon="faCopy" @click="copyToClipboard">Copy to clipboard</ActionButton>
<ActionButton v-if="!props.isIconOnly" variant="secondary" size="sm" :icon="faCopy" aria-label="Copy to clipboard" @click="copyToClipboard">Copy to clipboard</ActionButton>
<ActionButton v-else variant="secondary" size="sm" :icon="faCopy" tooltip="Copy to clipboard" @click="copyToClipboard" />
</Tippy>
</template>
14 changes: 7 additions & 7 deletions src/Frontend/src/components/configuration/EndpointConnection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,10 @@ async function serviceControlConnections() {
<!-- Nav tabs -->
<div v-if="!loading" class="tabs" role="tablist">
<h5 :class="{ active: showCodeOnlyTab }">
<a @click="switchCodeOnlyTab()" class="ng-binding">Endpoint configuration only</a>
<a @click="switchCodeOnlyTab()" class="ng-binding" role="link">Endpoint configuration only</a>
</h5>
<h5 :class="{ active: !showCodeOnlyTab }">
<a @click="switchJsonTab()" class="ng-binding">JSON file</a>
<a @click="switchJsonTab()" class="ng-binding" role="link">JSON file</a>
</h5>
</div>

Expand All @@ -119,24 +119,24 @@ async function serviceControlConnections() {
</ul>
</div>

<section v-if="showCodeOnlyTab && !loading">
<section v-if="showCodeOnlyTab && !loading" role="tabpanel" aria-label="Endpoint configuration only">
<div class="row">
<div class="col-12 h-100">
<CodeEditor :model-value="inlineSnippet" language="csharp" :show-gutter="false"></CodeEditor>
<CodeEditor :model-value="inlineSnippet" language="csharp" :show-gutter="false" :show-copy-to-clipboard="true"></CodeEditor>
</div>
</div>
</section>

<section v-if="!showCodeOnlyTab && !loading">
<section v-if="!showCodeOnlyTab && !loading" role="tabpanel" aria-label="JSON file">
<div class="row">
<div class="col-12 h-100">
<p>Note that when using JSON for configuration, you also need to change the endpoint configuration as shown below.</p>
<p><strong>Endpoint configuration:</strong></p>
<CodeEditor :model-value="jsonSnippet" language="csharp" :show-gutter="false"></CodeEditor>
<CodeEditor :model-value="jsonSnippet" language="csharp" :show-gutter="false" :show-copy-to-clipboard="true"></CodeEditor>
<p style="margin-top: 15px">
<strong>JSON configuration file:</strong>
</p>
<CodeEditor :model-value="jsonConfig" language="json" :show-gutter="false"></CodeEditor>
<CodeEditor :model-value="jsonConfig" language="json" :show-gutter="false" :show-copy-to-clipboard="true"></CodeEditor>
</div>
</div>
</section>
Expand Down
58 changes: 58 additions & 0 deletions src/Frontend/test/preconditions/endpointConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { SetupFactoryOptions } from "../driver";
import { ServiceControlInstanceConnection } from "@/components/serviceControlClient";
import { MetricsConnectionDetails } from "@/components/monitoring/monitoringClient";

export const hasServiceControlConnection =
(settings: Partial<ServiceControlInstanceConnection["settings"]> = {}) =>
({ driver }: SetupFactoryOptions) => {
const serviceControlUrl = window.defaultConfig.service_control_url;

const defaultSettings = {
Heartbeats: {
Enabled: true,
HeartbeatsQueue: "Particular.ServiceControl@XXX",
Frequency: "00:00:10",
TimeToLive: "00:00:40",
},
CustomChecks: {
Enabled: true,
CustomChecksQueue: "Particular.ServiceControl@XXX",
},
ErrorQueue: "error",
SagaAudit: {
Enabled: true,
SagaAuditQueue: "audit",
},
MessageAudit: {
Enabled: true,
AuditQueue: "audit",
},
...settings,
};

driver.mockEndpoint(`${serviceControlUrl}connection`, {
body: {
settings: defaultSettings,
errors: [],
} as any as ServiceControlInstanceConnection,

Check failure on line 37 in src/Frontend/test/preconditions/endpointConnection.ts

View workflow job for this annotation

GitHub Actions / Windows

Unexpected any. Specify a different type

Check failure on line 37 in src/Frontend/test/preconditions/endpointConnection.ts

View workflow job for this annotation

GitHub Actions / windows-standalone

Unexpected any. Specify a different type
});
};
export const hasMonitoringConnection =
(settings: Partial<MetricsConnectionDetails> = {}) =>
({ driver }: SetupFactoryOptions) => {
const monitoringUrl = window.defaultConfig.monitoring_urls[0];

const defaultSettings: MetricsConnectionDetails = {
Enabled: true,
MetricsQueue: "Particular.Monitoring",
Interval: "00:00:01",
...settings,
};

driver.mockEndpoint(`${monitoringUrl}connection`, {
body: {
Metrics: defaultSettings,
errors: [],
},
});
};
1 change: 1 addition & 0 deletions src/Frontend/test/preconditions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export { hasEndpointSettings } from "./hasEndpointSettings";
export * from "./configuration";
export * from "./authentication";
export * from "./platformCapabilities";
export * from "./endpointConnection";
126 changes: 126 additions & 0 deletions src/Frontend/test/specs/configuration/questions/endpointConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { screen, within, waitFor } from "@testing-library/vue";

/**
* Gets the "Endpoint Configuration Only" tab element
*/
export async function endpointConfigurationOnlyTab() {
// Find all h5 elements within the tabs div
const tabs = await screen.findByRole("tablist");
const endpointConfigTab = within(tabs).getByText(/Endpoint configuration only/i);
return endpointConfigTab.closest("h5");
}

/**
* Gets the "JSON File" tab element
*/
export async function jsonFileTab() {
const tabs = await screen.findByRole("tablist");
const jsonTab = within(tabs).getByText(/JSON file/i);
return jsonTab.closest("h5");
}

/**
* Checks if a selelcted tab is currently active
*/
export function isTabActive(tabElement: HTMLElement | null): boolean {
if (!tabElement) return false;
return tabElement.classList.contains("active");
}

/**
* Waits for the code editor to be rendered and have content
*/
export async function waitForCodeEditorContent(editorIndex = 0, expectedContent?: string, timeout = 5000): Promise<string> {
let content = "";

await waitFor(
() => {
content = getCodeEditorContent(editorIndex);

if (content.length === 0) {
throw new Error(`Code editor at index ${editorIndex} has no content`);
}

if (expectedContent && !content.includes(expectedContent)) {
throw new Error(`Code editor content does not contain expected text: "${expectedContent}". ` + `Got content (length ${content.length}): ${content.substring(0, 100)}...`);
}

return true;
},
{ timeout }
);

return content;
}

export function getCodeEditorContent(editorIndex = 0) {
const codeEditors = screen.queryAllByRole("code");

if (codeEditors.length === 0) {
return "";
}

if (editorIndex >= codeEditors.length) {
return "";
}

const codeEditor = codeEditors[editorIndex];

const vueInstance = (codeEditor as any).__vueParentComponent || (codeEditor as any).__vnode;

Check failure on line 69 in src/Frontend/test/specs/configuration/questions/endpointConnection.ts

View workflow job for this annotation

GitHub Actions / Windows

Unexpected any. Specify a different type

Check failure on line 69 in src/Frontend/test/specs/configuration/questions/endpointConnection.ts

View workflow job for this annotation

GitHub Actions / Windows

Unexpected any. Specify a different type

Check failure on line 69 in src/Frontend/test/specs/configuration/questions/endpointConnection.ts

View workflow job for this annotation

GitHub Actions / windows-standalone

Unexpected any. Specify a different type

Check failure on line 69 in src/Frontend/test/specs/configuration/questions/endpointConnection.ts

View workflow job for this annotation

GitHub Actions / windows-standalone

Unexpected any. Specify a different type
if (vueInstance) {
const modelValue = vueInstance?.props?.modelValue || vueInstance?.ctx?.modelValue || (vueInstance as any)?.__vModel;

Check failure on line 71 in src/Frontend/test/specs/configuration/questions/endpointConnection.ts

View workflow job for this annotation

GitHub Actions / Windows

Unexpected any. Specify a different type

Check failure on line 71 in src/Frontend/test/specs/configuration/questions/endpointConnection.ts

View workflow job for this annotation

GitHub Actions / windows-standalone

Unexpected any. Specify a different type
if (modelValue && typeof modelValue === "string") {
return modelValue;
}
}
return "";
}

/**
* Clicks on a tab to switch to it
*/
export function clickTab(tabElement: HTMLElement) {
const link = within(tabElement).getByRole("link");
link.click();
}
/**
* Gets all "Copy to clipboard" buttons on the page
*/
export function getCopyToClipboardButtons() {
const buttonsByText = Array.from(document.querySelectorAll("button")).filter((btn) => btn.textContent?.includes("Copy to clipboard"));

if (buttonsByText.length > 0) {
return buttonsByText;
}
return [];
}

export async function getVisibleCopyButton(index = 0) {
return await waitFor(
async () => {
const buttons = await getCopyToClipboardButtons();
if (buttons.length === 0) {
throw new Error("No copy buttons found on the page");
}

const button = buttons[index] as HTMLButtonElement;
const style = window.getComputedStyle(button);

// Basic check - just ensure it's not explicitly hidden
const isExplicitlyHidden = style.display === "none" || style.visibility === "hidden";

if (isExplicitlyHidden) {
throw new Error("Copy button is explicitly hidden");
}
return button;
},
{ timeout: 5000 }
);
}
/**
* Clicks the copy button for the currently active tab
*/
export async function clickCopyButton(index = 0) {
const button = await getVisibleCopyButton(index);
button.click();
}
Loading