Skip to content
Merged
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
71 changes: 59 additions & 12 deletions page-objects/api-contract-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export class ApiContractPage extends BasePage {

private readonly sidebarProcessBar: (specName: string) => Locator;

readonly infoDialog: Locator;

constructor(page: Page, testInfo: TestInfo, eyes: any, specName: string) {
super(page, testInfo, eyes, specName);
this.specTree = page.locator("#spec-tree");
Expand Down Expand Up @@ -218,6 +220,8 @@ export class ApiContractPage extends BasePage {
this.includeButton = this.specSection
.locator("button.clear")
.filter({ hasText: "Include" });

this.infoDialog = this.page.locator("#alert-container .alert-msg.info");
}

//Function Beginning
Expand Down Expand Up @@ -247,7 +251,7 @@ export class ApiContractPage extends BasePage {
);
}

async clickRunContractTests() {
async clickRunContractTests(expectSuccess: boolean = true) {
await test.step("Run Contract Tests", async () => {
try {
await expect(this._runButton).toBeEnabled({ timeout: 10000 });
Expand All @@ -260,19 +264,47 @@ export class ApiContractPage extends BasePage {

await takeAndAttachScreenshot(this.page, "clicked-run-contract-tests");

await this.waitForTestCompletion();
if (expectSuccess) {
await this.waitForTestCompletion();
} else {
await this.page.waitForTimeout(1000);
}
} catch (e) {
await takeAndAttachScreenshot(this.page, "error-in-run-contract-tests");
throw new Error(`Failed to click Run button:`);
}
});
}

async waitforDialogToDismiss(status: string | RegExp) {
try {
const appeared = await this.infoDialog
.waitFor({ state: "visible", timeout: 5000 })
.then(() => true)
.catch(() => false);

if (!appeared) {
console.log("[INFO] Dialog did not appear — safe to continue");
return;
}

await expect.soft(this.infoDialog).toContainText(status, {
timeout: 10000,
});

await this.infoDialog.waitFor({ state: "hidden", timeout: 5000 });
} catch (e) {
console.log("[WARN] Dialog wait issue — continuing:", e);
}
}

private async waitForTestCompletion() {
await this.waitForTestsToStartRunning();

await this.waitForTestsToCompleteExecution();

await this.waitforDialogToDismiss(/Tests? complete/i);

await takeAndAttachScreenshot(this.page, "test-completed", this.eyes);
}

Expand Down Expand Up @@ -376,6 +408,7 @@ export class ApiContractPage extends BasePage {
path: string,
method: string,
response: string,
{ captureVisual = true }: { captureVisual?: boolean } = {},
) {
const checkbox = this.exclusionCheckboxLocator(path, method, response);
try {
Expand All @@ -400,11 +433,13 @@ export class ApiContractPage extends BasePage {
if (!isChecked) {
await checkbox.click();
}
await takeAndAttachScreenshot(
this.page,
"excluded-test-" + `${path}-${method}-${response}`,
this.eyes,
);
if (captureVisual) {
await takeAndAttachScreenshot(
this.page,
"excluded-test-" + `${path}-${method}-${response}`,
this.eyes,
);
}
}

async clickExcludeButton() {
Expand Down Expand Up @@ -442,8 +477,14 @@ export class ApiContractPage extends BasePage {
testItem.path,
testItem.method,
testItem.response,
{ captureVisual: false },
);
}
await takeAndAttachScreenshot(
this.page,
`multiple-tests-selected-${testList.length}`,
this.eyes,
);
}

async getMixedOperationErrorText(): Promise<string> {
Expand Down Expand Up @@ -762,7 +803,9 @@ export class ApiContractPage extends BasePage {
const summary = this.getPrereqErrorSummary();
const message = this.getPrereqErrorMessage();

const isVisible = await summary.isVisible({ timeout: 3000 }).catch(() => false);
const isVisible = await summary
.isVisible({ timeout: 3000 })
.catch(() => false);
if (!isVisible) {
console.log("No prerequisite error detected.");
return;
Expand All @@ -775,9 +818,13 @@ export class ApiContractPage extends BasePage {

await summary.click();

await expect(message).toBeVisible({ timeout: 5000 }).catch(() => {
console.warn("Detail message did not become visible after clicking summary");
});
await expect(message)
.toBeVisible({ timeout: 5000 })
.catch(() => {
console.warn(
"Detail message did not become visible after clicking summary",
);
});

await takeAndAttachScreenshot(this.page, "contract-prereq-error-expanded");

Expand All @@ -787,6 +834,6 @@ export class ApiContractPage extends BasePage {
: "<not visible>";

console.error(`Prerequisite error summary: ${summaryText}`);
console.error(`Prerequisite error detail: ${detailedMessage}`);
console.error(`Prerequisite error detail: ${detailedMessage}`);
}
}
127 changes: 15 additions & 112 deletions page-objects/example-generation-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
import { takeAndAttachScreenshot } from "../utils/screenshotUtils";
import { BasePage } from "./base-page";
import { Edit } from "../utils/types/json-edit.types";
import { SpecEditorPage } from "./spec-editor-page";


export class ExampleGenerationPage extends BasePage {
readonly openApiTabPage: OpenAPISpecTabPage;
Expand All @@ -30,6 +32,8 @@ export class ExampleGenerationPage extends BasePage {
private readonly specSection: Locator;
private readonly specEditorSection: Locator;
private readonly specTabLocator: Locator;
private readonly specEditorHelper: SpecEditorPage;


constructor(page: Page, testInfo: TestInfo, eyes: any, specName: string) {
super(page, testInfo, eyes, specName);
Expand Down Expand Up @@ -62,8 +66,10 @@ export class ExampleGenerationPage extends BasePage {
this.bulkValidateBtnSelector = "button#bulk-validate";
this.inlineBtnSelector = "button#import";
this.bulkFixBtnSelector = "button#bulk-fix";
this.specEditorHelper = new SpecEditorPage(page);
}


private async openExampleGenerationTab() {
console.log("Opening Example Generation tab");
return this.openApiTabPage.openExampleGenerationTab();
Expand Down Expand Up @@ -1134,7 +1140,7 @@ export class ExampleGenerationPage extends BasePage {
await expect(editorContext.content).toBeVisible({ timeout: 15000 });
await editorContext.content.click();

await this.loadFullEditorDocument(editorContext.scroller);
await this.specEditorHelper.loadFullEditorDocument(editorContext.scroller);
await editorContext.scroller.evaluate((el) => {
el.scrollTop = 0;
});
Expand All @@ -1145,7 +1151,7 @@ export class ExampleGenerationPage extends BasePage {
: [`${exampleName}_response`];

for (const searchTerm of targets) {
const foundByEditorApi = await this.focusTermUsingCodeMirrorApi(
const foundByEditorApi = await this.specEditorHelper.focusTermUsingCodeMirrorApi(
editorContext.content,
searchTerm,
);
Expand All @@ -1157,12 +1163,18 @@ export class ExampleGenerationPage extends BasePage {
);

if (!foundByWindowFind) {
await this.scrollToEditorSearchTerm(
await this.specEditorHelper.scrollEditorToFindTerm(
editorContext.content,
editorContext.scroller,
editorContext.lines,
searchTerm,
);
// Click the matched line for visual cursor placement
const match = editorContext.lines.filter({ hasText: searchTerm }).first();
if ((await match.count()) > 0) {
await match.scrollIntoViewIfNeeded();
await match.click();
}
}

await this.page.waitForTimeout(250);
Expand Down Expand Up @@ -1230,113 +1242,4 @@ export class ExampleGenerationPage extends BasePage {
return window.find(term, false, false, true, false, false, false);
}, searchTerm);
}

private async focusTermUsingCodeMirrorApi(
content: Locator,
searchTerm: string,
): Promise<boolean> {
return await content.evaluate((el, term) => {
const cmEditor = el.closest(".cm-editor") as any;
const view = cmEditor?.cmView?.view;
if (!view) return false;

const fullText = view.state.doc.toString() as string;
const index = fullText.indexOf(term);
if (index === -1) return false;

view.dispatch({
selection: { anchor: index, head: index + term.length },
scrollIntoView: true,
});
return true;
}, searchTerm);
}

private async loadFullEditorDocument(scroller: Locator): Promise<void> {
await expect(scroller).toBeVisible({ timeout: 10000 });

let unchangedCount = 0;
let previousScrollHeight = -1;

for (let i = 0; i < 60; i++) {
const metrics = await scroller.evaluate((el) => {
el.scrollTop = el.scrollHeight;
return {
scrollTop: el.scrollTop,
scrollHeight: el.scrollHeight,
clientHeight: el.clientHeight,
};
});

if (metrics.scrollHeight === previousScrollHeight) {
unchangedCount += 1;
} else {
unchangedCount = 0;
}

previousScrollHeight = metrics.scrollHeight;

const atBottom =
metrics.scrollTop + metrics.clientHeight >= metrics.scrollHeight - 2;
if (atBottom && unchangedCount >= 2) {
break;
}

await this.page.waitForTimeout(120);
}
}

private async scrollToEditorSearchTerm(
content: Locator,
scroller: Locator,
lines: Locator,
searchTerm: string,
): Promise<void> {
await scroller.evaluate((el) => {
el.scrollTop = 0;
});
await this.page.waitForTimeout(120);

for (let i = 0; i < 250; i++) {
const match = lines.filter({ hasText: searchTerm }).first();
const count = await match.count();

if (count > 0) {
await match.scrollIntoViewIfNeeded();
await match.click();
return;
}

const moved = await scroller.evaluate((el) => {
const prev = el.scrollTop;
el.scrollTop = Math.min(
el.scrollTop + Math.max(el.clientHeight * 0.85, 120),
el.scrollHeight,
);
return el.scrollTop > prev;
});

if (!moved) {
break;
}

await this.page.waitForTimeout(80);
}

// Fallback: some editor layouts scroll via outer containers, not .cm-scroller.
await content.hover();
for (let i = 0; i < 280; i++) {
const visible = await lines.evaluateAll(
(els, term) => els.some((el) => el.textContent?.includes(term)),
searchTerm,
);
if (visible) {
return;
}
await this.page.mouse.wheel(0, 900);
await this.page.waitForTimeout(60);
}

throw new Error(`Could not find '${searchTerm}' in the spec editor`);
}
}
Loading
Loading