From 0fd2eeb8834b7356bd3f2c418d04558ee344a1eb Mon Sep 17 00:00:00 2001 From: Andrei Varabyeu Date: Wed, 26 Nov 2025 20:41:30 +0100 Subject: [PATCH 1/3] Create features/share-calculator-results.feature --- features/share-calculator-results.feature | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 features/share-calculator-results.feature diff --git a/features/share-calculator-results.feature b/features/share-calculator-results.feature new file mode 100644 index 0000000..9be04ce --- /dev/null +++ b/features/share-calculator-results.feature @@ -0,0 +1,11 @@ +Feature: Share Calculator Results Table + As a user of the Share/Stock Calculator + I want the results table to render accessibly and update dynamically + + Scenario: Results table renders correctly and is accessible after submitting valid inputs + Given I open the Share Calculator page + When I enter valid inputs for shares and date and submit the form + Then I should see an accessible results table rendered dynamically without a page reload + And a loading indicator should appear and disappear during calculation when present + And the results table should contain column headers and at least one result row + And the page URL should not change after submission From ca48df7164c1e2dd07d31ea401a9d89d7fc6b19d Mon Sep 17 00:00:00 2001 From: Andrei Varabyeu Date: Wed, 26 Nov 2025 20:41:42 +0100 Subject: [PATCH 2/3] Create src/pages/ShareCalculatorPage.ts --- src/pages/ShareCalculatorPage.ts | 82 ++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/pages/ShareCalculatorPage.ts diff --git a/src/pages/ShareCalculatorPage.ts b/src/pages/ShareCalculatorPage.ts new file mode 100644 index 0000000..c0bf76c --- /dev/null +++ b/src/pages/ShareCalculatorPage.ts @@ -0,0 +1,82 @@ +import { Page, Locator, expect } from '@playwright/test'; +import { BasePage } from './BasePage'; + +export class ShareCalculatorPage extends BasePage { + // Page URL + static readonly url = 'https://investorcenter.slb.com/stock/share-calculator'; + + // Locators - use robust selectors where possible + readonly sharesInput = 'input[name="shares"], input[id*="shares"], input[aria-label*="Shares"]'; + readonly dateInput = 'input[type="date"], input[name*="date"], input[id*="date"]'; + readonly submitButton = 'button[type="submit"], button:has-text("Calculate"), button:has-text("Submit")'; + readonly resultsTable = 'table:visible, [role="table"]:visible'; + readonly resultsTableHeader = `${this.resultsTable} th`; + readonly resultsTableRows = `${this.resultsTable} tbody tr`; + readonly loadingIndicator = '[aria-busy="true"], .loading, .spinner, .skeleton'; + readonly errorMessage = '[role="alert"], .error, .form-error'; + + constructor(page: Page) { + super(page); + } + + async navigate(): Promise { + await this.navigateTo(ShareCalculatorPage.url); + await this.waitForLoadState(); + } + + async fillShares(value: string): Promise { + const shares = this.page.locator(this.sharesInput).first(); + await shares.waitFor({ state: 'visible', timeout: 10000 }); + await shares.fill(value); + } + + async fillDate(value: string): Promise { + const date = this.page.locator(this.dateInput).first(); + await date.waitFor({ state: 'visible', timeout: 10000 }); + await date.fill(value); + } + + async submit(): Promise { + const submit = this.page.locator(this.submitButton).first(); + await submit.waitFor({ state: 'visible', timeout: 10000 }); + await Promise.all([ + this.page.waitForResponse(resp => resp.request().method() === 'POST' || resp.request().method() === 'GET', { timeout: 15000 }).catch(() => {}), + submit.click(), + ]); + } + + async waitForLoadingToFinish(timeout = 15000): Promise { + const loader = this.page.locator(this.loadingIndicator).first(); + try { + if (await loader.isVisible()) { + await loader.waitFor({ state: 'hidden', timeout }); + } + } catch { + // ignore + } + } + + async getResultsTableHeaders(): Promise { + const headers = await this.page.locator(this.resultsTableHeader).allTextContents(); + return headers.map(h => h.trim()).filter(h => h.length > 0); + } + + async getResultsRowCount(): Promise { + return await this.page.locator(this.resultsTableRows).count(); + } + + async isResultsTableAccessible(): Promise { + const table = this.page.locator(this.resultsTable).first(); + if (!(await table.count())) return false; + // Check for semantic markup + const role = await table.getAttribute('role'); + const ariaLabel = await table.getAttribute('aria-label'); + const hasThead = (await table.locator('thead').count()) > 0; + const hasTh = (await table.locator('th').count()) > 0; + return hasTh || hasThead || !!ariaLabel || role === 'table'; + } + + async hasError(): Promise { + return (await this.page.locator(this.errorMessage).count()) > 0; + } +} From d6f7c954a525edaa5bbe8b258b8c63bf8a586f2b Mon Sep 17 00:00:00 2001 From: Andrei Varabyeu Date: Wed, 26 Nov 2025 20:41:52 +0100 Subject: [PATCH 3/3] Create src/steps/share-calculator-steps.ts --- src/steps/share-calculator-steps.ts | 65 +++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/steps/share-calculator-steps.ts diff --git a/src/steps/share-calculator-steps.ts b/src/steps/share-calculator-steps.ts new file mode 100644 index 0000000..a381ecf --- /dev/null +++ b/src/steps/share-calculator-steps.ts @@ -0,0 +1,65 @@ +import { Given, When, Then } from '@cucumber/cucumber'; +import { expect } from '@playwright/test'; +import { getPage } from '../support/browser'; +import { ShareCalculatorPage } from '../pages/ShareCalculatorPage'; + +let pageObj: ShareCalculatorPage; + +Given('I open the Share Calculator page', async function () { + const page = getPage(); + pageObj = new ShareCalculatorPage(page); + await pageObj.navigate(); + // Ensure page loaded + const url = await pageObj.getUrl(); + expect(url).toContain('share-calculator'); +}); + +When('I enter valid inputs for shares and date and submit the form', async function () { + await pageObj.fillShares('100'); + await pageObj.fillDate('2023-06-15'); + // Capture URL before submit + (this as any).preSubmitUrl = await pageObj.getUrl(); + await pageObj.submit(); +}); + +Then('I should see an accessible results table rendered dynamically without a page reload', async function () { + const preUrl = (this as any).preSubmitUrl; + await pageObj.waitForLoadingToFinish(); + // URL should remain same (no page reload) + const postUrl = await pageObj.getUrl(); + expect(postUrl).toBe(preUrl); + + const hasTable = (await pageObj.getResultsRowCount()) > 0 && (await pageObj.getResultsTableHeaders()).length > 0; + expect(hasTable).toBeTruthy(); + const accessible = await pageObj.isResultsTableAccessible(); + expect(accessible).toBeTruthy(); +}); + +Then('a loading indicator should appear and disappear during calculation when present', async function () { + // If there is a loader, its visibility should toggle + const loader = await getPage().locator(pageObj.loadingIndicator).first(); + // We cannot deterministically assert it appears, but if it exists, ensure it hides eventually + if (await loader.count() > 0) { + try { + if (await loader.isVisible()) { + await loader.waitFor({ state: 'hidden', timeout: 15000 }); + } + } catch { + // if it never disappears, fail + expect(await loader.isVisible()).toBeFalsy(); + } + } +}); + +Then('the results table should contain column headers and at least one result row', async function () { + const headers = await pageObj.getResultsTableHeaders(); + const rows = await pageObj.getResultsRowCount(); + expect(headers.length).toBeGreaterThan(0); + expect(rows).toBeGreaterThan(0); +}); + +Then('the page URL should not change after submission', async function () { + const preUrl = (this as any).preSubmitUrl; + const postUrl = await pageObj.getUrl(); + expect(postUrl).toBe(preUrl); +});