diff --git a/test/system/src/appMap.ts b/test/system/src/appMap.ts index 01172ac9e..0fcea99cd 100644 --- a/test/system/src/appMap.ts +++ b/test/system/src/appMap.ts @@ -1,4 +1,6 @@ import { Locator, Page } from '@playwright/test'; +import FindingDetailsWebview from './findingDetailsWebview'; +import FindingsOverviewWebview from './findingsOverviewWebview'; import InstructionsWebview from './instructionsWebview'; export enum InstructionStep { @@ -18,7 +20,9 @@ export enum InstructionStepStatus { export default class AppMap { constructor( protected readonly page: Page, - protected readonly instructionsWebview: InstructionsWebview + protected readonly instructionsWebview: InstructionsWebview, + protected readonly findingsOverviewWebview: FindingsOverviewWebview, + protected readonly findingDetailsWebview: FindingDetailsWebview ) {} get actionPanelButton(): Locator { @@ -49,6 +53,10 @@ export default class AppMap { return this.findingsTree.locator('.pane-body >> [role="treeitem"]').nth(nth || 0); } + public finding(nth?: number): Locator { + return this.findingsTree.locator('[role="treeitem"][aria-level="3"]').nth(nth || 0); + } + public appMapTreeItem(): Locator { return this.appMapTree.locator('.pane-body >> [role="treeitem"]:not([aria-expanded])').first(); } @@ -59,6 +67,14 @@ export default class AppMap { } } + public async openFindingsOverview(): Promise { + this.findingsTreeItem(0).click(); + } + + public openNthFinding(nth: number): void { + this.finding(nth).click(); + } + public async openActionPanel(): Promise { await this.actionPanelButton.click(); await this.ready(); diff --git a/test/system/src/driver.ts b/test/system/src/driver.ts index 0262fa176..35e7789d5 100644 --- a/test/system/src/driver.ts +++ b/test/system/src/driver.ts @@ -1,6 +1,8 @@ import { BrowserContext, ElectronApplication, Locator, Page } from '@playwright/test'; import { glob } from 'glob'; import AppMap from './appMap'; +import FindingDetailsWebview from './findingDetailsWebview'; +import FindingsOverviewWebview from './findingsOverviewWebview'; import InstructionsWebview from './instructionsWebview'; import Panel from './panel'; import { getOsShortcut } from './util'; @@ -15,7 +17,14 @@ async function tryClick(elem: Locator, timeout = 5000) { export default class Driver { public readonly instructionsWebview = new InstructionsWebview(this.page); - public readonly appMap = new AppMap(this.page, this.instructionsWebview); + public readonly findingsOverviewWebview = new FindingsOverviewWebview(this.page); + public readonly findingDetailsWebview = new FindingDetailsWebview(this.page); + public readonly appMap = new AppMap( + this.page, + this.instructionsWebview, + this.findingsOverviewWebview, + this.findingDetailsWebview + ); public readonly panel = new Panel(this.page); constructor( diff --git a/test/system/src/findingDetailsWebview.ts b/test/system/src/findingDetailsWebview.ts new file mode 100644 index 000000000..0c17eb2f3 --- /dev/null +++ b/test/system/src/findingDetailsWebview.ts @@ -0,0 +1,31 @@ +import { FrameLocator, Page } from '@playwright/test'; +import { strictEqual } from 'assert'; +import { waitFor } from '../../waitFor'; + +export default class FindingDetailsWebview { + constructor(protected readonly page: Page) {} + + private frameSelector = 'iframe.webview.ready'; + private frame?: FrameLocator; + private initializeErrorMsg = 'No frame found. Call initialize() first'; + + public async assertTitleRenders(expectedTitle: string): Promise { + if (!this.frame) throw Error(this.initializeErrorMsg); + + const title = this.frame.locator('[data-cy="title"]'); + strictEqual(await title.count(), 1, 'Expected one title element'); + strictEqual(await title.innerText(), expectedTitle); + } + + public async initialize(expectedFrames: number): Promise { + const checkForIFrames = async () => { + const iframes = await this.page.locator(this.frameSelector).count(); + return iframes === expectedFrames; + }; + + await waitFor('waiting for second iframe', checkForIFrames.bind(this)); + const outerFrame = this.page.frameLocator(this.frameSelector).last(); + await outerFrame.locator('iframe#active-frame').waitFor(); + this.frame = outerFrame.frameLocator('#active-frame'); + } +} diff --git a/test/system/src/findingsOverviewWebview.ts b/test/system/src/findingsOverviewWebview.ts new file mode 100644 index 000000000..4b6a6be63 --- /dev/null +++ b/test/system/src/findingsOverviewWebview.ts @@ -0,0 +1,49 @@ +import { FrameLocator, Page } from '@playwright/test'; +import { strictEqual } from 'assert'; +import { waitFor } from '../../waitFor'; + +export default class FindingsOverviewWebview { + constructor(protected readonly page: Page) {} + + private title = 'Runtime Analysis'; + private frameSelector = 'iframe.webview.ready'; + private frame?: FrameLocator; + private initializeErrorMsg = 'No frame found. Call initialize() first'; + + public async assertNumberOfFindingsInOverview(expected: number): Promise { + if (!this.frame) throw Error(this.initializeErrorMsg); + + const count = await this.frame.locator('[data-cy="finding"]').count(); + strictEqual(count, expected, `Expected number of findings to be ${expected}`); + } + + public async assertTitleRenders(): Promise { + if (!this.frame) throw Error(this.initializeErrorMsg); + + const title = this.frame.locator('[data-cy="title"]'); + strictEqual(await title.count(), 1, 'Expected one title element'); + strictEqual(await title.innerText(), this.title); + } + + public async openFirstFindingDetail(): Promise { + if (!this.frame) throw Error(this.initializeErrorMsg); + + this.frame + .locator('[data-cy="finding"]') + .first() + .locator('ul') + .click(); + } + + public async initialize(expectedFrames: number): Promise { + const checkForIFrames = async () => { + const iframes = await this.page.locator(this.frameSelector).count(); + return iframes === expectedFrames; + }; + + await waitFor('waiting for second iframe', checkForIFrames.bind(this)); + const outerFrame = this.page.frameLocator(this.frameSelector).last(); + await outerFrame.locator('iframe#active-frame').waitFor(); + this.frame = outerFrame.frameLocator('#active-frame'); + } +} diff --git a/test/system/tests/findings.test.ts b/test/system/tests/findings.test.ts index 67a1f44fb..2743deb54 100644 --- a/test/system/tests/findings.test.ts +++ b/test/system/tests/findings.test.ts @@ -1,3 +1,4 @@ +import { strictEqual } from 'assert'; import * as path from 'path'; describe('Findings and scanning', function() { @@ -19,9 +20,88 @@ describe('Findings and scanning', function() { await driver.appMap.openActionPanel(); await driver.appMap.expandFindings(); await driver.appMap.findingsTree.click(); - await driver.appMap.findingsTreeItem().waitFor({ state: 'hidden' }); + await driver.appMap.findingsTreeItem().waitFor(); await project.restoreFiles('**/*.appmap.json'); await driver.waitForFile(path.join(project.workspacePath, 'tmp', '**', 'mtime')); // Wait for the indexer - await driver.appMap.findingsTreeItem().waitFor({ state: 'visible' }); + await driver.appMap.findingsTreeItem(1).waitFor(); + }); + + it('shows the findings overview page', async function() { + const { driver, project } = this; + + await driver.appMap.openActionPanel(); + await driver.appMap.expandFindings(); + await driver.appMap.openFindingsOverview(); + const expectedFrames = 2; + await driver.appMap.findingsOverviewWebview.initialize(expectedFrames); + await driver.appMap.findingsOverviewWebview.assertTitleRenders(); + await driver.appMap.findingsOverviewWebview.assertNumberOfFindingsInOverview(0); + await project.restoreFiles('**/*.appmap.json'); + await driver.waitForFile(path.join(project.workspacePath, 'tmp', '**', 'mtime')); // Wait for the indexer + await driver.appMap.findingsTreeItem(1).waitFor(); + await driver.appMap.findingsOverviewWebview.assertNumberOfFindingsInOverview(3); + }); + + it('opens the findings details page from the findings overview page', async function() { + const { driver, project } = this; + + await driver.appMap.openActionPanel(); + await driver.appMap.expandFindings(); + await driver.appMap.openFindingsOverview(); + + let expectedFrames = 2; + await driver.appMap.findingsOverviewWebview.initialize(expectedFrames); + await driver.appMap.findingsOverviewWebview.assertTitleRenders(); + await project.restoreFiles('**/*.appmap.json'); + await driver.waitForFile(path.join(project.workspacePath, 'tmp', '**', 'mtime')); // Wait for the indexer + await driver.appMap.findingsTreeItem(1).waitFor(); + await driver.appMap.findingsOverviewWebview.openFirstFindingDetail(); + + expectedFrames = 3; + await driver.appMap.findingDetailsWebview.initialize(expectedFrames); + + const expectedTitle = 'N plus 1 SQL query'; + await driver.appMap.findingDetailsWebview.assertTitleRenders(expectedTitle); + }); + + it('opens the findings details page from the runtime analysis tree view', async function() { + const { driver, project } = this; + + await driver.appMap.openActionPanel(); + await driver.appMap.expandFindings(); + + await project.restoreFiles('**/*.appmap.json'); + await driver.waitForFile(path.join(project.workspacePath, 'tmp', '**', 'mtime')); // Wait for the indexer + await driver.appMap.findingsTreeItem(1).waitFor(); + + await driver.appMap.openNthFinding(2); + const expectedFrames = 2; + await driver.appMap.findingDetailsWebview.initialize(expectedFrames); + + const expectedTitle = 'N plus 1 SQL query'; + await driver.appMap.findingDetailsWebview.assertTitleRenders(expectedTitle); + }); + + it('reuses the finding details webview', async function() { + const { driver, project } = this; + + await driver.appMap.openActionPanel(); + await driver.appMap.expandFindings(); + await driver.appMap.openFindingsOverview(); + let expectedFrames = 2; + await driver.appMap.findingsOverviewWebview.initialize(expectedFrames); + + await project.restoreFiles('**/*.appmap.json'); + await driver.waitForFile(path.join(project.workspacePath, 'tmp', '**', 'mtime')); // Wait for the indexer + await driver.appMap.findingsTreeItem(1).waitFor(); + + await driver.appMap.findingsOverviewWebview.openFirstFindingDetail(); + expectedFrames = 3; + await driver.appMap.findingDetailsWebview.initialize(expectedFrames); + await driver.appMap.openNthFinding(2); + await driver.appMap.findingDetailsWebview.initialize(expectedFrames); + + const numTabs = await driver.tabCount(); + strictEqual(numTabs, expectedFrames); }); });