Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add integration test fixtures #280

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "browser integration test fixtures",
"packageName": "@itwin/browser-authorization",
"email": "[email protected]",
"dependentChangeType": "none"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "electron integration test fixtures",
"packageName": "@itwin/electron-authorization",
"email": "[email protected]",
"dependentChangeType": "none"
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,18 @@ import { User } from "oidc-client-ts";
import { AuthType } from "../types";
import type { SignInOptions } from "../types";

export class TestHelper {
constructor(private _signInOptions: SignInOptions) {}
export class BrowserAppFixture {
constructor(private _page: Page, private _signInOptions: SignInOptions) { }

public async signIn(page: Page) {
get routes() {
return {
staticCallback: `${this._signInOptions.url}?callbackFromStorage=true`,
authViaPopup: `${this._signInOptions.url}/signin-via-popup`,
root: `${this._signInOptions.url}`,
}
}

public async signIn(page: Page = this._page) {
await page.getByLabel("Email address").fill(this._signInOptions.email);
await page.getByLabel("Email address").press("Enter");
await page.getByLabel("Password").fill(this._signInOptions.password);
Expand All @@ -24,8 +32,35 @@ export class TestHelper {
}
}

public async getUserFromLocalStorage(page: Page): Promise<User> {
const storageState = await page.context().storageState();
public async signInViaPopup() {
await this._page.goto(this.routes.authViaPopup);
const popupPromise = this._page.waitForEvent("popup");
const el = this._page.getByText("Signin via Popup");
await el.click();
const popup = await popupPromise;
await popup.waitForLoadState();

const signInPromise = this.signIn(popup);
const closeEventPromise = popup.waitForEvent("close");

await Promise.all([signInPromise, closeEventPromise]);
}

public async signOutViaPopup() {
const signoutPopupPromise = this._page.waitForEvent("popup");
const locator = this._page.getByTestId("signout-button-popup");
await locator.click();
const signOutPopup = await signoutPopupPromise;
return signOutPopup
}

public async signOut() {
const locator = this._page.getByTestId("signout-button");
await locator.click();
}

public async getUserFromLocalStorage(): Promise<User> {
const storageState = await this._page.context().storageState();
const localStorage = storageState.origins.find(
(o) => o.origin === this._signInOptions.url
)?.localStorage;
Expand All @@ -44,12 +79,11 @@ export class TestHelper {
}

public async validateAuthenticated(
page: Page,
authType: AuthType = AuthType.Redirect
) {
const locator = page.getByTestId("content");
const locator = this._page.getByTestId("content");
await expect(locator).toContainText("Authorized");
const user = await this.getUserFromLocalStorage(page);
const user = await this.getUserFromLocalStorage();
expect(user.access_token).toBeDefined();

let url = `${this._signInOptions.url}/`;
Expand All @@ -58,10 +92,23 @@ export class TestHelper {
if (authType === AuthType.RedirectStatic)
url = "http://localhost:5173/?callbackFromStorage=true";

expect(page.url()).toEqual(url);
expect(this._page.url()).toEqual(url);
}

public async validateUnauthenticated(page: Page = this._page) {
const content = page.getByText("Sign Off Successful");
await expect(content).toBeVisible();
}

public async goToPage(url: string) {
await this._page.goto(url);
}

public async waitForPageLoad(url: string = this._signInOptions.url, page: Page = this._page) {
await page.waitForURL(url);
}

private async handleConsentScreen(page: Page) {
private async handleConsentScreen(page: Page = this._page) {
const consentAcceptButton = page.getByRole("link", {
name: "Accept",
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { test as base } from "@playwright/test";
import { BrowserAppFixture } from "./BrowserAppFixture";
import { loadConfig } from "../helpers/loadConfig";

const { url, clientId, envPrefix, email, password } = loadConfig();

const signInOptions = {
email,
password,
url,
clientId,
envPrefix: envPrefix || "",
};

export const test = base.extend<{ app: BrowserAppFixture }>({
app: async ({ page }, use) => {
const testHelper = new BrowserAppFixture(page, signInOptions);
await use(testHelper);
},
});

export { expect } from "@playwright/test";

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/

// eslint@typescript-eslint/naming-convention
import { config } from "dotenv";

export function loadConfig() {
Expand Down
105 changes: 26 additions & 79 deletions packages/browser/src/integration-tests/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,93 +3,40 @@
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/

import { expect, test } from "@playwright/test";
import { TestHelper } from "./helpers/TestHelper";
import { AuthType } from "./types";
import type { SignInOptions } from "./types";
import { loadConfig } from "./helpers/loadConfig";
import { test } from "./fixtures/BrowserFixtures";

const { url, clientId, envPrefix, email, password } = loadConfig();

const signInOptions: SignInOptions = {
email,
password,
url,
clientId,
envPrefix: envPrefix || "",
};

const testHelper = new TestHelper(signInOptions);

test("signin redirect", async ({ page }) => {
await page.goto(signInOptions.url);
await testHelper.signIn(page);
await page.waitForURL(signInOptions.url);

await testHelper.validateAuthenticated(page);
test("signin redirect", async ({ app }) => {
await app.goToPage(app.routes.root)
await app.signIn();
await app.waitForPageLoad(app.routes.root)
await app.validateAuthenticated();
});

test("signin redirect - callback settings from storage", async ({ page }) => {
const staticCallbackUrl = `${signInOptions.url}?callbackFromStorage=true`;
await page.goto(staticCallbackUrl);
await testHelper.signIn(page);
await page.waitForURL(staticCallbackUrl);

await testHelper.validateAuthenticated(page, AuthType.RedirectStatic);
test("signin redirect - callback settings from storage", async ({ app }) => {
await app.goToPage(app.routes.staticCallback);
await app.signIn();
await app.waitForPageLoad(app.routes.staticCallback)
await app.validateAuthenticated(AuthType.RedirectStatic);
});

test("signout redirect", async ({ page }) => {
await page.goto(signInOptions.url);
await testHelper.signIn(page);
await page.waitForURL(signInOptions.url);

const locator = page.getByTestId("signout-button");
await locator.click();

const content = page.getByText("Sign Off Successful");
expect(content).toBeDefined();

// Cannot get the below working on QA...
// We'll have to settle for the above check
// await expect(content).toContainText("Signed Out!");
// const user = await testHelper.getUserFromLocalStorage(page);
// expect(user).not.toBeDefined();
test("signout redirect", async ({ app }) => {
await app.goToPage(app.routes.root)
await app.signIn();
await app.waitForPageLoad(app.routes.root)
await app.validateAuthenticated()
await app.signOut();
await app.validateUnauthenticated()
});

test("signin popup", async ({ page }) => {
await page.goto(`${signInOptions.url}/signin-via-popup`);
const popupPromise = page.waitForEvent("popup");
const el = page.getByText("Signin via Popup");
await el.click();
const popup = await popupPromise;
await popup.waitForLoadState();

const signInPromise = testHelper.signIn(popup);
const closeEventPromise = popup.waitForEvent("close");

await Promise.all([signInPromise, closeEventPromise]);
await testHelper.validateAuthenticated(page, AuthType.PopUp);
test("signin popup", async ({ app }) => {
await app.signInViaPopup();
await app.validateAuthenticated(AuthType.PopUp);
});

test("signout popup", async ({ page }) => {
await page.goto(`${signInOptions.url}/signin-via-popup`);
const popupPromise = page.waitForEvent("popup");
const el = page.getByText("Signin via Popup");
await el.click();
const popup = await popupPromise;
await popup.waitForLoadState();

const signInPromise = testHelper.signIn(popup);
const closeEventPromise = popup.waitForEvent("close");

await Promise.all([signInPromise, closeEventPromise]);
await testHelper.validateAuthenticated(page, AuthType.PopUp);

const signoutPopupPromise = page.waitForEvent("popup");
const locator = page.getByTestId("signout-button-popup");
await locator.click();
const signOutPopup = await signoutPopupPromise;

const content = signOutPopup.getByText("Sign Off Successful");
expect(content).toBeDefined();
test("signout popup", async ({ app }) => {
await app.signInViaPopup();
await app.validateAuthenticated(AuthType.PopUp);
const signOutPopup = await app.signOutViaPopup()
await app.validateUnauthenticated(signOutPopup);
});
3 changes: 2 additions & 1 deletion packages/electron/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ module.exports = [
...iTwinPlugin.configs.iTwinjsRecommendedConfig,
},
{
files: ["**/*.{ts,tsx}"],
files: ["src/**/*.{ts,tsx}"],
...iTwinPlugin.configs.jsdocConfig,
ignores: ["src/integration-test/**/*", "src/test/**/*"],
},
{
files: ["**/*.{ts,tsx}"],
Expand Down
46 changes: 46 additions & 0 deletions packages/electron/src/integration-test/fixtures/AuthFixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Page } from "@playwright/test";
import type { RefreshTokenStore } from "../../main/TokenStore";

interface AuthFixtureProps {
page: Page;
tokenStore: RefreshTokenStore;
}

export class AuthFixture {
private _page: Page;
private _tokenStore: RefreshTokenStore;

constructor(options: AuthFixtureProps) {
this._page = options.page;
this._tokenStore = options.tokenStore;
}

public async signInIMS(url: string, email: string, password: string) {
await this._page.goto(url);
await this._page.waitForSelector("#identifierInput", { timeout: 5000 });
await this._page.getByLabel("Email address").fill(email);
await this._page.getByLabel("Email address").press("Enter");
await this._page.getByLabel("Password").fill(password);
await this._page.getByText("Sign In").click();

const consentUrl = this._page.url();
if (consentUrl.endsWith("resume/as/authorization.ping")) {
await this.handleConsentScreen();
}
}

// Admittedly this is cheating: no user would interact
// with the tokenStore directly, but this is a tough
// case to test otherwise.
public async switchScopes(scopes: string) {
await this._tokenStore.load(scopes);
}

private async handleConsentScreen() {
const consentAcceptButton = this._page.getByRole("link", {
name: "Accept",
});
if (consentAcceptButton)
await consentAcceptButton.click();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { ElectronApplication, Page } from "@playwright/test";

export class ElectronAppFixture {
constructor(private _page: Page, private _app: ElectronApplication) { }

public get buttons() {
return {
signIn: this._page.getByTestId("signIn"),
signOut: this._page.getByTestId("signOut"),
status: this._page.getByTestId("getStatus"),
};
}

/**
* Signs in, captures the URL electron opens internally, and returns for IMS.
*/
public async clickSignIn(): Promise<string> {
await this._page.waitForSelector("button#signIn");
const button = this._page.getByTestId("signIn");
const clickPromise = button.click();
const urlPromise = this.getUrl();
const [, url] = await Promise.all([clickPromise, urlPromise]);
return url;
}

public async clickSignOut() {
await this._page.waitForSelector("button#signOut");
const button = this._page.getByTestId("signOut");
await button.click();
}

public async checkStatus(expectedStatus: boolean) {
await this._page.waitForSelector("button#getStatus");
const button = this._page.getByTestId("getStatus");
await button.click();
this._page.getByText(expectedStatus ? "signed in" : "signed out");
}

/**
* Captures the URL that electron opens internally using its shell module.
*/
public async getUrl(): Promise<string> {
return this._app.evaluate<string>(async ({ shell }) => {
return new Promise((resolve) => {
shell.openExternal = async (url: string) => {
return resolve(url);
};
});
});
}

public async destroy() {
await this._page.close();
await this._app.close();
}
}
Loading
Loading