Skip to content

Commit

Permalink
chore(testkit): add playwright test drivers (#2561)
Browse files Browse the repository at this point in the history
  • Loading branch information
uziab authored Oct 30, 2024
1 parent da71c1b commit 574a339
Show file tree
Hide file tree
Showing 42 changed files with 18,465 additions and 16,678 deletions.
24 changes: 24 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,27 @@ jobs:
env:
SINCE_FLAG: ${{ steps.determine-since-flag.outputs.since_flag }}
run: yarn lerna run ${{ matrix.command }} $SINCE_FLAG

tests_e2e:
name: Run end-to-end tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run Setup
uses: ./.github/actions/setup
with:
npm_token: ${{ secrets.npm_token }}
- id: determine-since-flag
uses: ./.github/actions/determine-lerna-since-flag
- uses: ./.github/actions/download-builds
- name: Install playwright browsers
run: npx playwright install --with-deps
- name: Run tests
run: yarn lerna run test:e2e $SINCE_FLAG --scope "@vibe/testkit"
- uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
name: test-results
path: packages/testing-kit/reports
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ dist
coverage
.scannerwork
*.env
.DS_Store
.DS_Store
packages/testkit/test-results
packages/testkit/reports
129 changes: 129 additions & 0 deletions packages/testkit/BaseElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { test, Page, Locator } from "@playwright/test";

/**
* Class representing a base element for Playwright tests.
*/
export class BaseElement {
page : Page;
locator : Locator;
elementReportName: String
/**
* Create a BaseElement.
* @param {Object} page - The Playwright page object.
* @param {Object} locator - The locator for the element.
* @param {string} elementReportName - The name for reporting purposes.
*/
constructor(page: Page, locator: Locator, elementReportName: String) {
this.page = page;
this.locator = locator;
this.elementReportName = elementReportName;
}

/**
* Get the locator of the element.
* @returns {Object} - The locator of the element.
*/
getLocator() {
return this.locator;
}

/**
* Wait for the list elements to stabilize (i.e., the count of items remains constant for a specified duration).
* @returns {Promise<void>}
*/
async waitForElementsGroup(locator: Locator, elementReportName: String): Promise<void> {
await test.step(`Wait for ${elementReportName} items to stabilize`, async () => {
let previousCount = 0;
let stableCountTime = 0;
const stabilizationTimeMs = 500;
while (true) {
const currentCount = await this.locator.locator(locator).count();

if (currentCount === previousCount) {
stableCountTime += 100; // Increase stable time by the check interval (100ms)
} else {
stableCountTime = 0; // Reset if the count changes
}
if (stableCountTime >= stabilizationTimeMs) {
break; // Break the loop if count has been stable for the desired duration
}
previousCount = currentCount;
await this.page.waitForTimeout(100); // Polling interval (100ms)
}
});
}

/**
* Check if the element is enabled.
* @returns {Promise<boolean>} - Returns true if the element is enabled, otherwise false.
*/
async isEnabled(): Promise<boolean> {
let isEnabled: boolean = false;
await test.step(`Return if ${this.elementReportName} is enabled`, async () => {
isEnabled = await this.locator.isEnabled();
return isEnabled;
});
return isEnabled;
}

/**
* Scroll the element into view if needed.
* @returns {Promise<void>}
*/
async scrollIntoView(): Promise<void> {
await test.step(`Scroll ${this.elementReportName} into view`, async () => {
await this.locator.scrollIntoViewIfNeeded();
});
}
async getAttributeValue(attributeName: string, options: any = { timeout: 10000, pollInterval: 500 }) : Promise <string | null> {
let attributeValue = null;

await test.step(`Get attribute ${attributeName} of ${this.elementReportName}`, async () => {
const startTime = Date.now();

while (Date.now() - startTime < options.timeout) {
attributeValue = await this.locator.getAttribute(attributeName);

if (attributeValue !== null) {
break;
}
while (Date.now() - startTime < options.timeout) {
await new Promise(resolve => setTimeout(resolve, options.pollInterval));
}
if (attributeValue === null) {
throw new Error(`Attribute ${attributeName} did not exist after ${options.timeout} ms`);
}
}
});
return attributeValue;
}

async getText() : Promise<string|undefined> {
let text: string|undefined;
await test.step(`Get text of ${this.elementReportName}`, async () => {
text = await this.locator.innerText();
return text;
});
return text;
}

async waitFor(options = {}): Promise<void> {
await test.step(`Wait for ${this.elementReportName}`, async () => {
await this.locator.waitFor(options);
});
}

async waitForAbsence() : Promise<void>{
await test.step(`Wait for ${this.elementReportName} to be absent`, async () => {
await this.waitFor({ state: "detached" });
});
}

async count() : Promise<number>{
let count: number=0;
await test.step(`Count elements matching ${this.elementReportName}`, async () => {
count = await this.locator.count();
});
return count;
}
}
11 changes: 11 additions & 0 deletions packages/testkit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# `@vibe/testkit`

> TODO: description
## Usage

```
const testingKit = require('@vibe/testkit');
// TODO: DEMONSTRATE API
```
41 changes: 41 additions & 0 deletions packages/testkit/__TESTS__/button.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { test, expect } from '@playwright/test';
import { Button } from '../buttons/Button';
import { buttonStory } from './utils/url-helper';

test('should fire a click event and log to console', async ({ page }) => {
// Navigate to the Storybook page with the component
await page.goto(buttonStory, {timeout: 100000});
// Locate the iframe where the button is rendered
const frame = page.frameLocator("[id='storybook-preview-iframe']");
const button = new Button(page, frame.locator('button[data-testid="button"]'), 'Button');

//TODO - find a better way to wait for the storybook to load
while (await button.locator.isVisible() === false) {
await page.waitForTimeout(30000);
await page.reload();
if (await button.locator.isVisible() === true) {
break;
}
}
// Add a listener to capture console logs
let consoleMessage = '';
page.on('console', async (msg) => {
const values = await Promise.all(msg.args().map(arg => arg.jsonValue()));
consoleMessage = values.join(' ');
});

// Attach a click event listener that logs a message to the console
await button.locator.evaluate((buttonElement) => {
buttonElement.addEventListener('click', () => {
console.log('Button clicked'); // Log to console when clicked
});
});
// Click the button
await button.click();

// Wait a bit to ensure the console log is captured
await page.waitForTimeout(500);

// Verify the console log contains the expected message
expect(consoleMessage).toContain('Button clicked');
});
73 changes: 73 additions & 0 deletions packages/testkit/__TESTS__/buttonGroup.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { test, expect } from "@playwright/test";
import { ButtonGroup } from "../buttons/ButtonGroup";
import { buttonGroupStory } from "./utils/url-helper";


test.describe("ButtonGroup Class with Storybook", () => {
let buttonGroup;

test.beforeEach(async ({ page }) => {
// Navigate to the Storybook story where the ButtonGroup component is rendered
await page.goto(buttonGroupStory);
//TODO - find a better way to wait for the storybook to load
// Locate the iframe where the Storybook component is rendered
const frame = page.frameLocator("[id='storybook-preview-iframe']");

// Locate the button group inside the iframe
const buttonGroupLocator = frame.locator('div[data-testid="button-group"]');
while (await buttonGroupLocator.isVisible() === false) {
await page.waitForTimeout(30000);
await page.reload();
if (await buttonGroupLocator.isVisible() === true) {
break;
}
}
// Initialize ButtonGroup with the Playwright page and locator
buttonGroup = new ButtonGroup(page, buttonGroupLocator, "Test Button Group");
});

test("should initialize buttons if needed", async () => {
// Initialize the buttons inside the ButtonGroup
await buttonGroup.initializeButtonsIfNeeded();

// Verify that buttons are initialized
expect(buttonGroup.buttonsInitialized).toBe(true);

// Verify that buttons exist in the items array
expect(buttonGroup.items.length).toBeGreaterThan(0); // Ensure that buttons were initialized
});

test("should retrieve a button by name and click it", async ({ page }) => {
// Initialize the buttons inside the ButtonGroup
await buttonGroup.initializeButtonsIfNeeded();

const button = await buttonGroup.getButtonByName("Beta"); // Adjust the button name as needed

// Add a listener for console logs to capture the click event
let consoleMessage = "";
page.on("console", async msg => {
const values = await Promise.all(msg.args().map(arg => arg.jsonValue()));
consoleMessage = values.join(" ");
});

// Attach a click listener that logs a message to the console when the button is clicked
await button.locator.evaluate(buttonElement => {
buttonElement.addEventListener("click", () => {
console.log("Button clicked: Beta"); // Log to console when clicked
});
});

// Click the button
await buttonGroup.click("Beta");

// Wait a bit to ensure the console log is captured
await page.waitForTimeout(500);

// Verify the console message contains the expected log
expect(consoleMessage).toContain("Button clicked: Beta");
});

test("should throw an error if trying to click a non-existent button", async () => {
await expect(buttonGroup.click("NonExistentButton")).rejects.toThrow("Invalid button name provided");
});
});
24 changes: 24 additions & 0 deletions packages/testkit/__TESTS__/checkbox.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { test, expect } from "@playwright/test";
import { Checkbox } from "../inputs/Checkbox";
import { checkboxStory } from "./utils/url-helper";

test.describe("menuButton Class with Storybook", () => {
let checkbox;

test.beforeEach(async ({ page }) => {
await page.goto(checkboxStory);
const frame = page.frameLocator("[id='storybook-preview-iframe']");
const checkboxLocator = frame.locator('[data-testid="checkbox-checkbox"]');
checkbox = new Checkbox(page, checkboxLocator, "Test checkbox button");
});

test("set checkbox", async ({page}) => {
if (await checkbox.isChecked()){
await checkbox.setChecked(false);
expect(await checkbox.isChecked()).toBe(false);
}else {
await checkbox.setChecked(true);
expect(await checkbox.isChecked()).toBe(true);
}
});
});
21 changes: 21 additions & 0 deletions packages/testkit/__TESTS__/dropdown.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { test, expect } from "@playwright/test";
import { Dropdown } from "../inputs/Dropdown";
import { dropdownStory } from "./utils/url-helper";


test.describe("dropdown Class with Storybook", () => {
let DropDown;

test.beforeEach(async ({ page }) => {
await page.goto(dropdownStory);
const frame = page.frameLocator("[id='storybook-preview-iframe']");
const DropDownLocator = frame.locator('[id="dropdown-menu-id"]');
DropDown = new Dropdown(page, DropDownLocator, "Test DropDown");
});

test("set dropdown value", async ({page}) => {
await DropDown.selectItem("2");
await page.waitForTimeout(500);
expect(await DropDown.getText()).toContain("Option 2");
});
});
23 changes: 23 additions & 0 deletions packages/testkit/__TESTS__/menuButton.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { test, expect } from "@playwright/test";
import { MenuButton } from "../buttons/MenuButton";
import { menuButtonStory } from "./utils/url-helper";

test.describe("menuButton Class with Storybook", () => {
let menuButton;

test.beforeEach(async ({ page }) => {
await page.goto(menuButtonStory);
const frame = page.frameLocator("[id='storybook-preview-iframe']");
const menubuttonLocator = frame.locator('[data-testid="menu-button"]');
menuButton = new MenuButton(page, menubuttonLocator, "Test menu button");
});

test("should open and close menu", async ({page}) => {
await menuButton.openMenu();
await page.waitForTimeout(500);
expect(await menuButton.isExpanded()).toBe(true);
await menuButton.closeMenu();
await page.waitForTimeout(500);
expect(await menuButton.isExpanded()).toBe(false);
});
});
20 changes: 20 additions & 0 deletions packages/testkit/__TESTS__/textArea.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { test, expect } from "@playwright/test";
import { TextArea } from "../inputs/TextArea";
import { textAreaStory } from "./utils/url-helper";

test.describe("textArea Class with Storybook", () => {
let textArea;
let textAreaLocator

test.beforeEach(async ({ page }) => {
await page.goto(textAreaStory);
const frame = page.frameLocator("[id='storybook-preview-iframe']");
textAreaLocator = frame.locator('[data-testid="text-area"]');
textArea = new TextArea(page, textAreaLocator, "Test text field");
});

test("set text in textarea", async ({page}) => {
await textArea.setText("Test Text");
expect(textArea.textAreaInput.locator).toHaveValue("Test Text");
});
});
Loading

0 comments on commit 574a339

Please sign in to comment.