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
36 changes: 36 additions & 0 deletions .github/workflows/validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Validation

on:
push:
branches-ignore:
- main
pull_request:
branches:
- main

permissions:
contents: read

jobs:
validation:
name: Typecheck & Build
runs-on: ubuntu-24.04

steps:
- name: Checkout repository
uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 24
cache: npm

- name: Install dependencies
run: npm ci

- name: Typecheck
run: npm run typecheck

- name: Build
run: npm run build
2 changes: 2 additions & 0 deletions lib/browser.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type BrowserName = "chrome" | "firefox";
export type BrowserMode = "headless" | "display";
67 changes: 67 additions & 0 deletions lib/test.context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { WebDriver } from "selenium-webdriver";

import Pages from "@pages/index";
import {
WebDriverFactory,
type WebDriverFactoryOptions,
} from "@lib/webdriver.factory";
import { createTestMetadata, type TestMetadata } from "@lib/test.metadata";
import { recordTestMetadata } from "@lib/test.execution.metadata";

export default class TestContext {
private _driver?: WebDriver;
private _pages?: Pages;
private _metadata?: TestMetadata;

constructor(private readonly options: WebDriverFactoryOptions = {}) {}

async start(): Promise<void> {
if (this._driver) {
throw new Error("TestContext has already been started.");
}

const factory = new WebDriverFactory(this.options);
this._driver = await factory.build();
this._metadata = createTestMetadata(factory.metadata);
this._pages = new Pages(this._driver);
recordTestMetadata(this._metadata);
}

get driver(): WebDriver {
if (!this._driver) {
throw new Error("TestContext has not been started. Call start() first.");
}

return this._driver;
}

get pages(): Pages {
if (!this._pages) {
throw new Error("TestContext has not been started. Call start() first.");
}

return this._pages;
}

get metadata(): TestMetadata {
if (!this._metadata) {
throw new Error("TestContext has not been started. Call start() first.");
}

return this._metadata;
}

async stop(): Promise<void> {
if (!this._driver) {
return;
}

try {
await this._driver.quit();
} finally {
this._driver = undefined;
this._pages = undefined;
this._metadata = undefined;
}
}
}
25 changes: 25 additions & 0 deletions lib/test.execution.metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import fs from "node:fs";
import path from "node:path";

import type { TestMetadata } from "@lib/test.metadata";

let executionMetadata: TestMetadata | undefined;

export function recordTestMetadata(metadata: TestMetadata) {
if (executionMetadata) {
return;
}

executionMetadata = metadata;
const reportsDirectory = path.resolve("reports");
fs.mkdirSync(reportsDirectory, { recursive: true });
fs.writeFileSync(
path.join(reportsDirectory, "metadata.json"),
`${JSON.stringify(metadata, null, 2)}\n`,
"utf8",
);
}

export function getTestMetadata(): TestMetadata | undefined {
return executionMetadata;
}
99 changes: 99 additions & 0 deletions lib/test.metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import os from "node:os";
import type { BrowserMode, BrowserName } from "@lib/browser.types";

export type TestExecutionEnvironment = "local" | "ci";

export interface TestMetadataInput {
browser: {
name: BrowserName;
version: string;
mode: BrowserMode;
};

platform: {
name: string;
};
}

export interface TestMetadata extends TestMetadataInput {
platform: {
name: string;
architecture: string;
};

runtime: {
node: string;
selenium: string;
mocha: string;
};

execution: {
environment: TestExecutionEnvironment;
startedAt: string;
};

sourceControl: {
branch?: string;
commit?: string;
};
}

function getExecutionEnvironment(): TestExecutionEnvironment {
return process.env.CI === "true" ? "ci" : "local";
}

function getGitBranch(): string | undefined {
return (
process.env.GITHUB_HEAD_REF ||
process.env.GITHUB_REF_NAME ||
process.env.GIT_BRANCH ||
undefined
);
}

function getGitCommit(): string | undefined {
return process.env.GITHUB_SHA || process.env.GIT_COMMIT || undefined;
}

function getDependencyVersion(packageName: string): string {
try {
const packageJson = require(`${packageName}/package.json`) as {
version: string;
};

return packageJson.version;
} catch {
return "unknown";
}
}

export function createTestMetadata(input: TestMetadataInput): TestMetadata {
return {
browser: {
name: input.browser.name,
version: input.browser.version,
mode: input.browser.mode,
},

platform: {
name: input.platform.name,
architecture: os.arch(),
},

runtime: {
node: process.version,
selenium: getDependencyVersion("selenium-webdriver"),
mocha: getDependencyVersion("mocha"),
},

execution: {
environment: getExecutionEnvironment(),
startedAt: new Date().toISOString(),
},

sourceControl: {
branch: getGitBranch(),
commit: getGitCommit(),
},
};
}
32 changes: 28 additions & 4 deletions lib/webdriver.factory.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { Builder, WebDriver } from "selenium-webdriver";
import chrome from "selenium-webdriver/chrome";
import firefox from "selenium-webdriver/firefox";
import type { BrowserMode, BrowserName } from "@lib/browser.types";
import type { TestMetadataInput } from "@lib/test.metadata";
import env from "@config/env";

export type BrowserName = "chrome" | "firefox";
export type BrowserMode = "headless" | "display";

export interface WebDriverFactoryOptions {
name?: BrowserName;
mode?: BrowserMode;
}

export class WebDriverFactory {
private _metadata?: TestMetadataInput;

constructor(private options: WebDriverFactoryOptions = {}) {}

async build(): Promise<WebDriver> {
const { name = env.browser.name, mode = env.browser.mode } = this.options;

const builder = new Builder().forBrowser(name);

if (name === "chrome") {
Expand All @@ -40,6 +42,28 @@ export class WebDriverFactory {
builder.setFirefoxOptions(firefoxOptions);
}

return await builder.build();
const driver = await builder.build();
const capabilities = await driver.getCapabilities();

this._metadata = {
browser: {
name: capabilities.get("browserName") as BrowserName,
version: capabilities.get("browserVersion") as string,
mode,
},
platform: {
name: capabilities.get("platformName") as string,
},
};

return driver;
}

get metadata(): TestMetadataInput {
if (!this._metadata) {
throw new Error("WebDriverFactory has not built a WebDriver yet.");
}

return this._metadata;
}
}
4 changes: 2 additions & 2 deletions pages/admin/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import LoginPage from "@pages/admin/auth/login.page";
export default class Auth {
private readonly loginPage: LoginPage;

constructor(public driver: WebDriver) {
this.loginPage = new LoginPage(driver);
constructor(private driver: WebDriver) {
this.loginPage = new LoginPage(this.driver);
}

async login() {
Expand Down
2 changes: 1 addition & 1 deletion pages/admin/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import BasePage from "@pages/base.page";

export default class Admin extends BasePage {
export default abstract class Admin extends BasePage {
get contentHeader() {
return this.getDataTest("content-header");
}
Expand Down
9 changes: 0 additions & 9 deletions pages/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { WebDriver } from "selenium-webdriver";

import Admin from "@pages/admin";
import Auth from "@pages/admin/auth";

import CustomerPage from "@pages/admin/customer";
Expand All @@ -9,24 +8,16 @@ import LoginPage from "@pages/admin/auth/login.page";

export default class Pages {
readonly auth: Auth;
readonly admin: Admin;

readonly login: LoginPage;
readonly dashboard: DashboardPage;
readonly customer: CustomerPage;

constructor(private driver: WebDriver) {
this.auth = new Auth(this.driver);
this.admin = new Admin(this.driver);

this.login = new LoginPage(this.driver);
this.dashboard = new DashboardPage(this.driver);
this.customer = new CustomerPage(this.driver);
}

async quit() {
if (this.driver != null) {
await this.driver.quit();
}
}
}
Loading
Loading